@luckydye/calendar 1.3.1 → 1.3.2
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 +1801 -1317
- package/package.json +1 -1
- package/src/CalDAVSource.ts +52 -19
- package/src/CalendarIntegration.ts +2 -2
- package/src/CalendarInternal.ts +1 -0
- package/src/CalendarView.ts +240 -36
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +2 -1
- package/src/ICal.ts +7 -2
- package/src/InMemorySource.ts +6 -6
- package/src/IndexedDBStorage.ts +3 -0
- package/src/InhouseBookingSource.ts +2 -1
- package/src/app.ts +155 -44
package/package.json
CHANGED
package/src/CalDAVSource.ts
CHANGED
|
@@ -74,13 +74,18 @@ export class CalDAVSource implements CalendarSource {
|
|
|
74
74
|
* Make an authenticated request to the CalDAV server.
|
|
75
75
|
*/
|
|
76
76
|
async request(url: string, method: string, body?: string, headers: Record<string, string> = {}): Promise<Response> {
|
|
77
|
+
const requestHeaders: Record<string, string> = {
|
|
78
|
+
'Authorization': this.getAuthHeader(),
|
|
79
|
+
...headers,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (body && !Object.keys(requestHeaders).some((key) => key.toLowerCase() === 'content-type')) {
|
|
83
|
+
requestHeaders['Content-Type'] = 'application/xml; charset=utf-8';
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
const response = await fetch(url, {
|
|
78
87
|
method,
|
|
79
|
-
headers:
|
|
80
|
-
'Authorization': this.getAuthHeader(),
|
|
81
|
-
'Content-Type': 'application/xml; charset=utf-8',
|
|
82
|
-
...headers,
|
|
83
|
-
},
|
|
88
|
+
headers: requestHeaders,
|
|
84
89
|
body,
|
|
85
90
|
});
|
|
86
91
|
|
|
@@ -242,7 +247,7 @@ export class CalDAVSource implements CalendarSource {
|
|
|
242
247
|
/**
|
|
243
248
|
* Fetch calendar objects (events) from a specific calendar.
|
|
244
249
|
*/
|
|
245
|
-
private async fetchCalendarObjects(calendarUrl: string): Promise<string
|
|
250
|
+
private async fetchCalendarObjects(calendarUrl: string): Promise<Array<{ href: string; icalData: string }>> {
|
|
246
251
|
const response = await this.request(
|
|
247
252
|
calendarUrl,
|
|
248
253
|
'REPORT',
|
|
@@ -263,11 +268,17 @@ export class CalDAVSource implements CalendarSource {
|
|
|
263
268
|
|
|
264
269
|
const text = await response.text();
|
|
265
270
|
|
|
266
|
-
const
|
|
267
|
-
const
|
|
271
|
+
const calendarObjects: Array<{ href: string; icalData: string }> = [];
|
|
272
|
+
const responseRegex = /<[^:]+:response[^>]*>([\s\S]*?)<\/[^:]+:response>/g;
|
|
273
|
+
|
|
274
|
+
for (const match of text.matchAll(responseRegex)) {
|
|
275
|
+
const responseBlock = match[1];
|
|
276
|
+
const hrefMatch = responseBlock.match(/<[^:]+:href[^>]*>(.*?)<\/[^:]+:href>/);
|
|
277
|
+
const calendarDataMatch = responseBlock.match(/<[^:]+:calendar-data[^>]*>([\s\S]*?)<\/[^:]+:calendar-data>/);
|
|
278
|
+
if (!hrefMatch || !calendarDataMatch) continue;
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
let icalData =
|
|
280
|
+
const href = new URL(hrefMatch[1], this.credentials.serverUrl).toString();
|
|
281
|
+
let icalData = calendarDataMatch[1].trim();
|
|
271
282
|
// Unescape XML entities
|
|
272
283
|
icalData = icalData
|
|
273
284
|
.replace(/</g, '<')
|
|
@@ -277,11 +288,11 @@ export class CalDAVSource implements CalendarSource {
|
|
|
277
288
|
.replace(/'/g, "'");
|
|
278
289
|
|
|
279
290
|
if (icalData) {
|
|
280
|
-
|
|
291
|
+
calendarObjects.push({ href, icalData });
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
|
|
284
|
-
return
|
|
295
|
+
return calendarObjects;
|
|
285
296
|
}
|
|
286
297
|
|
|
287
298
|
|
|
@@ -366,7 +377,13 @@ export class CalDAVSource implements CalendarSource {
|
|
|
366
377
|
/**
|
|
367
378
|
* Map a parsed VCALENDAR event to CalendarEvent.
|
|
368
379
|
*/
|
|
369
|
-
private mapToCalendarEvent(
|
|
380
|
+
private mapToCalendarEvent(
|
|
381
|
+
parsed: Partial<CalendarEvent>,
|
|
382
|
+
calendarDisplayName: string,
|
|
383
|
+
calendarUrl: string,
|
|
384
|
+
calendarColor?: string,
|
|
385
|
+
resourceUrl?: string
|
|
386
|
+
): CalendarEvent | null {
|
|
370
387
|
if (!parsed.start || !parsed.id) return null;
|
|
371
388
|
|
|
372
389
|
return {
|
|
@@ -387,6 +404,7 @@ export class CalDAVSource implements CalendarSource {
|
|
|
387
404
|
readOnly: false,
|
|
388
405
|
isAllDay: parsed.isAllDay,
|
|
389
406
|
reminders: parsed.reminders,
|
|
407
|
+
resourceUrl,
|
|
390
408
|
};
|
|
391
409
|
}
|
|
392
410
|
|
|
@@ -395,9 +413,21 @@ export class CalDAVSource implements CalendarSource {
|
|
|
395
413
|
*/
|
|
396
414
|
async fetchEventsForCalendar(calendarUrl: string, displayName: string, color?: string): Promise<CalendarEvent[]> {
|
|
397
415
|
const calendarObjects = await this.fetchCalendarObjects(calendarUrl);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
416
|
+
const events: CalendarEvent[] = [];
|
|
417
|
+
|
|
418
|
+
for (const calendarObject of calendarObjects) {
|
|
419
|
+
const event = this.mapToCalendarEvent(
|
|
420
|
+
parseSingleICalEvent(calendarObject.icalData),
|
|
421
|
+
displayName,
|
|
422
|
+
calendarUrl,
|
|
423
|
+
color,
|
|
424
|
+
calendarObject.href
|
|
425
|
+
);
|
|
426
|
+
if (!event) continue;
|
|
427
|
+
events.push(event);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return events;
|
|
401
431
|
}
|
|
402
432
|
|
|
403
433
|
/**
|
|
@@ -440,6 +470,7 @@ export class CalDAVSource implements CalendarSource {
|
|
|
440
470
|
await this.request(eventUrl, 'PUT', icalData, {
|
|
441
471
|
'Content-Type': 'text/calendar; charset=utf-8',
|
|
442
472
|
});
|
|
473
|
+
fullEvent.resourceUrl = eventUrl;
|
|
443
474
|
|
|
444
475
|
return fullEvent;
|
|
445
476
|
}
|
|
@@ -448,10 +479,11 @@ export class CalDAVSource implements CalendarSource {
|
|
|
448
479
|
* Update an existing event on the CalDAV server.
|
|
449
480
|
* Derives the calendar URL from updates.calendarId, falling back to the event cache.
|
|
450
481
|
*/
|
|
451
|
-
async updateEvent(
|
|
452
|
-
const
|
|
482
|
+
async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
|
|
483
|
+
const id = event.id;
|
|
484
|
+
const calendarUrl = updates.calendarId ?? event.calendarId;
|
|
453
485
|
if (!calendarUrl) throw new Error(`Cannot update event ${id}: calendar URL unknown`);
|
|
454
|
-
const eventUrl = `${calendarUrl.replace(/\/$/, '')}/${id}.ics`;
|
|
486
|
+
const eventUrl = updates.resourceUrl ?? event.resourceUrl ?? `${calendarUrl.replace(/\/$/, '')}/${id}.ics`;
|
|
455
487
|
|
|
456
488
|
// For updates, we need to fetch the existing event first
|
|
457
489
|
// This is a simplified implementation
|
|
@@ -480,6 +512,7 @@ export class CalDAVSource implements CalendarSource {
|
|
|
480
512
|
readOnly: false,
|
|
481
513
|
isAllDay: updates.isAllDay !== undefined ? updates.isAllDay : existing.isAllDay,
|
|
482
514
|
reminders: updates.reminders !== undefined ? updates.reminders : existing.reminders,
|
|
515
|
+
resourceUrl: updates.resourceUrl ?? eventUrl,
|
|
483
516
|
};
|
|
484
517
|
|
|
485
518
|
const icalData = this.serializeEventToICal(updatedEvent);
|
|
@@ -11,7 +11,7 @@ export interface Calendar {
|
|
|
11
11
|
calendarUrl?: string;
|
|
12
12
|
fetchEvents(): Promise<CalendarEvent[]>;
|
|
13
13
|
createEvent?(event: Omit<CalendarEvent, 'calendar' | 'color'>): Promise<CalendarEvent>;
|
|
14
|
-
updateEvent?(
|
|
14
|
+
updateEvent?(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
15
15
|
deleteEvent?(id: string): Promise<void>;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@ export interface CalendarSource {
|
|
|
28
28
|
enabled: boolean;
|
|
29
29
|
fetchEvents(): Promise<CalendarEvent[]>;
|
|
30
30
|
createEvent?(event: Omit<CalendarEvent, 'calendar' | 'color'>): Promise<CalendarEvent>;
|
|
31
|
-
updateEvent?(
|
|
31
|
+
updateEvent?(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
32
32
|
deleteEvent?(id: string): Promise<void>;
|
|
33
33
|
}
|
|
34
34
|
|
package/src/CalendarInternal.ts
CHANGED
package/src/CalendarView.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { serializeEventsToICal } from "./ICal.js";
|
|
|
18
18
|
import { queueStatus } from "./StatusMessage.js";
|
|
19
19
|
import type { StatusBarData } from "./StatusBar.js";
|
|
20
20
|
import { IndexedDBStorage } from "./IndexedDBStorage.js";
|
|
21
|
+
import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
|
|
21
22
|
import { TIME_SCALE_DAY_HEIGHT, type CalendarLayer, type LayerContext } from "./CalendarLayer.js";
|
|
22
23
|
import { createGridLayer } from "./layers/GridLayer.js";
|
|
23
24
|
import { createEventsLayer, type EventRect, type EventsState } from "./layers/EventsLayer.js";
|
|
@@ -259,7 +260,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
259
260
|
left: 60px;
|
|
260
261
|
right: 12px;
|
|
261
262
|
pointer-events: none;
|
|
262
|
-
z-index:
|
|
263
|
+
z-index: 101;
|
|
263
264
|
overflow: hidden;
|
|
264
265
|
}
|
|
265
266
|
|
|
@@ -300,7 +301,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
300
301
|
flex-direction: row;
|
|
301
302
|
backdrop-filter: blur(10px);
|
|
302
303
|
box-shadow: var(--shadow-overlay, 0 4px 12px rgba(0, 0, 0, 0.9));
|
|
303
|
-
z-index:
|
|
304
|
+
z-index: 102;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
.event-detail-header {
|
|
@@ -679,6 +680,19 @@ export class CalendarViewElement extends LitElement {
|
|
|
679
680
|
text-align: left;
|
|
680
681
|
}
|
|
681
682
|
|
|
683
|
+
.description-actions {
|
|
684
|
+
display: flex;
|
|
685
|
+
gap: 12px;
|
|
686
|
+
align-items: center;
|
|
687
|
+
flex-wrap: wrap;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.description-actions .description-see-more {
|
|
691
|
+
display: inline-block;
|
|
692
|
+
margin-top: 8px;
|
|
693
|
+
padding: 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
682
696
|
.description-see-more:hover {
|
|
683
697
|
color: var(--accent-hover, rgb(120, 170, 255));
|
|
684
698
|
}
|
|
@@ -992,6 +1006,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
992
1006
|
height: number;
|
|
993
1007
|
} | null = null;
|
|
994
1008
|
isDescriptionExpanded = false;
|
|
1009
|
+
descriptionSummaryTargetKey: string | null = null;
|
|
1010
|
+
descriptionSummaryText = "";
|
|
1011
|
+
descriptionSummaryLoading = false;
|
|
1012
|
+
descriptionSummaryError: string | null = null;
|
|
1013
|
+
private requestedDescriptionSummaryKey: string | null = null;
|
|
995
1014
|
|
|
996
1015
|
notificationPopoverOpen = false;
|
|
997
1016
|
scheduledNotifications: any[] = [];
|
|
@@ -1359,6 +1378,97 @@ export class CalendarViewElement extends LitElement {
|
|
|
1359
1378
|
this.repositionEventDetailOverlay();
|
|
1360
1379
|
}
|
|
1361
1380
|
|
|
1381
|
+
clearDescriptionSummary(): void {
|
|
1382
|
+
if (this.requestedDescriptionSummaryKey) {
|
|
1383
|
+
this.dispatchEvent(
|
|
1384
|
+
new CustomEvent("cancel-description-summary", {
|
|
1385
|
+
detail: { key: this.requestedDescriptionSummaryKey },
|
|
1386
|
+
bubbles: true,
|
|
1387
|
+
composed: true,
|
|
1388
|
+
}),
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
this.requestedDescriptionSummaryKey = null;
|
|
1392
|
+
this.descriptionSummaryTargetKey = null;
|
|
1393
|
+
this.descriptionSummaryText = "";
|
|
1394
|
+
this.descriptionSummaryLoading = false;
|
|
1395
|
+
this.descriptionSummaryError = null;
|
|
1396
|
+
this.requestUpdate();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
private getDescriptionSummaryTargetKey(event: CalendarEvent): string {
|
|
1400
|
+
return `${event.id}:${sanitizeEventDescription(event.description ?? "")}`;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private requestDescriptionSummary(event: CalendarEvent): void {
|
|
1404
|
+
const description = sanitizeEventDescription(event.description ?? "").trim();
|
|
1405
|
+
if (description.length <= 200) return;
|
|
1406
|
+
|
|
1407
|
+
const targetKey = this.getDescriptionSummaryTargetKey(event);
|
|
1408
|
+
this.clearDescriptionSummary();
|
|
1409
|
+
this.requestedDescriptionSummaryKey = targetKey;
|
|
1410
|
+
this.descriptionSummaryTargetKey = targetKey;
|
|
1411
|
+
this.descriptionSummaryText = "";
|
|
1412
|
+
this.descriptionSummaryLoading = true;
|
|
1413
|
+
this.descriptionSummaryError = null;
|
|
1414
|
+
this.dispatchEvent(
|
|
1415
|
+
new CustomEvent("request-description-summary", {
|
|
1416
|
+
detail: {
|
|
1417
|
+
event,
|
|
1418
|
+
key: targetKey,
|
|
1419
|
+
description,
|
|
1420
|
+
},
|
|
1421
|
+
bubbles: true,
|
|
1422
|
+
composed: true,
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1425
|
+
this.requestUpdate();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
startDescriptionSummary(key: string): void {
|
|
1429
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1430
|
+
this.descriptionSummaryText = "";
|
|
1431
|
+
this.descriptionSummaryLoading = true;
|
|
1432
|
+
this.descriptionSummaryError = null;
|
|
1433
|
+
this.requestUpdate();
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
appendDescriptionSummaryChunk(key: string, chunk: string): void {
|
|
1437
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1438
|
+
if (!chunk) return;
|
|
1439
|
+
this.descriptionSummaryText += chunk;
|
|
1440
|
+
this.descriptionSummaryLoading = true;
|
|
1441
|
+
this.requestUpdate();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
finishDescriptionSummary(key: string, error: string | null = null): void {
|
|
1445
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1446
|
+
this.descriptionSummaryLoading = false;
|
|
1447
|
+
this.descriptionSummaryError = error;
|
|
1448
|
+
this.requestUpdate();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
failDescriptionSummary(key: string, message: string): void {
|
|
1452
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1453
|
+
this.descriptionSummaryLoading = false;
|
|
1454
|
+
this.descriptionSummaryError = message;
|
|
1455
|
+
if (!this.descriptionSummaryText && this.selectedEventForDetail?.description) {
|
|
1456
|
+
this.descriptionSummaryText = this.selectedEventForDetail.description.replaceAll(
|
|
1457
|
+
"\\n",
|
|
1458
|
+
"\n",
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
this.requestUpdate();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
cancelDescriptionSummary(key: string): void {
|
|
1465
|
+
if (key !== this.descriptionSummaryTargetKey) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
this.descriptionSummaryLoading = false;
|
|
1469
|
+
this.requestUpdate();
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1362
1472
|
repositionEventDetailOverlay(): void {
|
|
1363
1473
|
if (!this.selectedEventForDetail || !this.selectedEventRect) return;
|
|
1364
1474
|
|
|
@@ -2254,6 +2364,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
2254
2364
|
this.resizingOriginalStart = null;
|
|
2255
2365
|
this.resizingOriginalEnd = null;
|
|
2256
2366
|
this.isResizingEvent = false;
|
|
2367
|
+
if (this.scrollContainer) {
|
|
2368
|
+
this.scrollContainer.style.cursor = "";
|
|
2369
|
+
}
|
|
2257
2370
|
this.renderCanvas();
|
|
2258
2371
|
this.requestUpdate();
|
|
2259
2372
|
}
|
|
@@ -2273,6 +2386,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2273
2386
|
// Close event detail overlay
|
|
2274
2387
|
this.selectedEventForDetail = null;
|
|
2275
2388
|
this.selectedEventRect = null;
|
|
2389
|
+
this.clearDescriptionSummary();
|
|
2276
2390
|
|
|
2277
2391
|
if (hadSelection) {
|
|
2278
2392
|
this.dispatchEvent(
|
|
@@ -2323,6 +2437,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2323
2437
|
this.internal.clearSelection();
|
|
2324
2438
|
this.selectedEventForDetail = null;
|
|
2325
2439
|
this.selectedEventRect = null;
|
|
2440
|
+
this.clearDescriptionSummary();
|
|
2326
2441
|
this.dispatchEvent(
|
|
2327
2442
|
new CustomEvent("selection-change", {
|
|
2328
2443
|
detail: { selectedEvents: [] },
|
|
@@ -2355,6 +2470,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2355
2470
|
this.selectedEventForDetail = null;
|
|
2356
2471
|
this.selectedEventRect = null;
|
|
2357
2472
|
this.isDescriptionExpanded = false;
|
|
2473
|
+
this.clearDescriptionSummary();
|
|
2358
2474
|
this.renderCanvas();
|
|
2359
2475
|
this.requestUpdate();
|
|
2360
2476
|
this.dispatchEvent(
|
|
@@ -2379,7 +2495,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
2379
2495
|
// Check for resize handles first — suppressed when alt key is active
|
|
2380
2496
|
const resizeHandle = !e.altKey ? this.getResizeHandle(x, y) : null;
|
|
2381
2497
|
if (resizeHandle && !this.isResizingEvent && !this.isDraggingEvent) {
|
|
2382
|
-
this.scrollContainer.style.cursor =
|
|
2498
|
+
this.scrollContainer.style.cursor =
|
|
2499
|
+
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
2383
2500
|
} else if (!this.isResizingEvent && !this.isDraggingEvent) {
|
|
2384
2501
|
this.scrollContainer.style.cursor = "";
|
|
2385
2502
|
}
|
|
@@ -2518,11 +2635,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
2518
2635
|
} else {
|
|
2519
2636
|
// Move event
|
|
2520
2637
|
this.dispatchEvent(
|
|
2521
|
-
new CustomEvent("
|
|
2638
|
+
new CustomEvent("update-event", {
|
|
2522
2639
|
detail: {
|
|
2523
2640
|
event: this.movingEvent,
|
|
2524
|
-
start: newStart,
|
|
2525
|
-
end: newEnd,
|
|
2641
|
+
updates: { start: newStart, end: newEnd },
|
|
2526
2642
|
},
|
|
2527
2643
|
bubbles: true,
|
|
2528
2644
|
}),
|
|
@@ -2682,6 +2798,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2682
2798
|
y: number,
|
|
2683
2799
|
): { event: CalendarEvent; edge: "start" | "end" } | null {
|
|
2684
2800
|
const RESIZE_HANDLE_SIZE = 8; // pixels from edge to detect resize
|
|
2801
|
+
const isZoomedIn = this.dayHeight >= TIME_SCALE_DAY_HEIGHT;
|
|
2685
2802
|
|
|
2686
2803
|
// Check in reverse order (top to bottom rendering)
|
|
2687
2804
|
for (let i = this.eventRects.length - 1; i >= 0; i--) {
|
|
@@ -2691,18 +2808,33 @@ export class CalendarViewElement extends LitElement {
|
|
|
2691
2808
|
// Skip read-only events
|
|
2692
2809
|
if (rect.event.readOnly) continue;
|
|
2693
2810
|
|
|
2694
|
-
|
|
2695
|
-
|
|
2811
|
+
if (isZoomedIn) {
|
|
2812
|
+
// In time-scale view, resize vertically from the top/bottom edges.
|
|
2813
|
+
if (x < rect.x || x > rect.x + rect.width) continue;
|
|
2696
2814
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2815
|
+
if (y >= rect.y && y <= rect.y + RESIZE_HANDLE_SIZE) {
|
|
2816
|
+
return { event: rect.event, edge: "start" };
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
if (
|
|
2820
|
+
y >= rect.y + rect.height - RESIZE_HANDLE_SIZE &&
|
|
2821
|
+
y <= rect.y + rect.height
|
|
2822
|
+
) {
|
|
2823
|
+
return { event: rect.event, edge: "end" };
|
|
2824
|
+
}
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// In zoomed-out view, resize horizontally from the left/right edges.
|
|
2829
|
+
if (y < rect.y || y > rect.y + rect.height) continue;
|
|
2830
|
+
|
|
2831
|
+
if (x >= rect.x && x <= rect.x + RESIZE_HANDLE_SIZE) {
|
|
2699
2832
|
return { event: rect.event, edge: "start" };
|
|
2700
2833
|
}
|
|
2701
2834
|
|
|
2702
|
-
// Check bottom edge (resize end)
|
|
2703
2835
|
if (
|
|
2704
|
-
|
|
2705
|
-
|
|
2836
|
+
x >= rect.x + rect.width - RESIZE_HANDLE_SIZE &&
|
|
2837
|
+
x <= rect.x + rect.width
|
|
2706
2838
|
) {
|
|
2707
2839
|
return { event: rect.event, edge: "end" };
|
|
2708
2840
|
}
|
|
@@ -2742,6 +2874,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
2742
2874
|
this.resizingOriginalStart = new Date(resizeHandle.event.start);
|
|
2743
2875
|
this.resizingOriginalEnd = new Date(resizeHandle.event.end);
|
|
2744
2876
|
this.isResizingEvent = false; // Will be set to true on first mouse move
|
|
2877
|
+
this.scrollContainer.style.cursor =
|
|
2878
|
+
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
2745
2879
|
return;
|
|
2746
2880
|
}
|
|
2747
2881
|
|
|
@@ -2764,6 +2898,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2764
2898
|
// Close event detail overlay
|
|
2765
2899
|
this.selectedEventForDetail = null;
|
|
2766
2900
|
this.selectedEventRect = null;
|
|
2901
|
+
this.clearDescriptionSummary();
|
|
2767
2902
|
|
|
2768
2903
|
this.movingEvent = clickedEvent;
|
|
2769
2904
|
this.movingEventOrigin = { x, y };
|
|
@@ -2779,6 +2914,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2779
2914
|
// Close event detail overlay when clicking outside of events
|
|
2780
2915
|
this.selectedEventForDetail = null;
|
|
2781
2916
|
this.selectedEventRect = null;
|
|
2917
|
+
this.clearDescriptionSummary();
|
|
2782
2918
|
|
|
2783
2919
|
// Clear selection when clicking on empty space
|
|
2784
2920
|
const hadSelection = this.internal.getSelectedEvents().length > 0;
|
|
@@ -2986,6 +3122,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2986
3122
|
// Close event detail overlay when replacing selection
|
|
2987
3123
|
this.selectedEventForDetail = null;
|
|
2988
3124
|
this.selectedEventRect = null;
|
|
3125
|
+
this.clearDescriptionSummary();
|
|
2989
3126
|
}
|
|
2990
3127
|
for (const event of selectedEvents) {
|
|
2991
3128
|
this.internal.selectEvent(event, "add");
|
|
@@ -3195,6 +3332,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3195
3332
|
|
|
3196
3333
|
// Show event detail overlay for single selection
|
|
3197
3334
|
if (!isCmdOrCtrl) {
|
|
3335
|
+
this.clearDescriptionSummary();
|
|
3198
3336
|
this.selectedEventForDetail = event;
|
|
3199
3337
|
this.isDescriptionExpanded = false;
|
|
3200
3338
|
// Find the event's rect and store it for positioning
|
|
@@ -3214,6 +3352,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3214
3352
|
this.selectedEventForDetail = null;
|
|
3215
3353
|
this.selectedEventRect = null;
|
|
3216
3354
|
this.isDescriptionExpanded = false;
|
|
3355
|
+
this.clearDescriptionSummary();
|
|
3217
3356
|
this.requestUpdate();
|
|
3218
3357
|
}
|
|
3219
3358
|
|
|
@@ -3275,6 +3414,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3275
3414
|
// Close event detail overlay when clearing selection
|
|
3276
3415
|
this.selectedEventForDetail = null;
|
|
3277
3416
|
this.selectedEventRect = null;
|
|
3417
|
+
this.clearDescriptionSummary();
|
|
3278
3418
|
|
|
3279
3419
|
if (hadSelection) {
|
|
3280
3420
|
this.dispatchEvent(
|
|
@@ -4122,6 +4262,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4122
4262
|
|
|
4123
4263
|
this.selectedEventForDetail = null;
|
|
4124
4264
|
this.selectedEventRect = null;
|
|
4265
|
+
this.clearDescriptionSummary();
|
|
4125
4266
|
this.requestUpdate();
|
|
4126
4267
|
}
|
|
4127
4268
|
|
|
@@ -4261,6 +4402,31 @@ export class CalendarViewElement extends LitElement {
|
|
|
4261
4402
|
}
|
|
4262
4403
|
|
|
4263
4404
|
const event = this.selectedEventForDetail;
|
|
4405
|
+
const eventDescription = sanitizeEventDescription(event.description ?? "");
|
|
4406
|
+
const eventDescriptionText = eventDescription.replaceAll("\\n", "\n");
|
|
4407
|
+
const useDescriptionSummary = eventDescription.trim().length > 200;
|
|
4408
|
+
const currentSummaryKey = this.getDescriptionSummaryTargetKey(event);
|
|
4409
|
+
const hasRequestedSummaryForCurrentEvent =
|
|
4410
|
+
this.requestedDescriptionSummaryKey === currentSummaryKey;
|
|
4411
|
+
const isSummaryForCurrentEvent =
|
|
4412
|
+
this.descriptionSummaryTargetKey === currentSummaryKey;
|
|
4413
|
+
const hasSummaryStateForCurrentEvent =
|
|
4414
|
+
hasRequestedSummaryForCurrentEvent || isSummaryForCurrentEvent;
|
|
4415
|
+
const streamedSummaryText = hasSummaryStateForCurrentEvent
|
|
4416
|
+
? this.descriptionSummaryText
|
|
4417
|
+
: "";
|
|
4418
|
+
const isSummaryLoading =
|
|
4419
|
+
useDescriptionSummary && hasSummaryStateForCurrentEvent && this.descriptionSummaryLoading;
|
|
4420
|
+
const shouldRenderSummary =
|
|
4421
|
+
useDescriptionSummary && hasSummaryStateForCurrentEvent;
|
|
4422
|
+
const descriptionToRender = shouldRenderSummary
|
|
4423
|
+
? streamedSummaryText || (isSummaryLoading ? "Generating summary..." : eventDescriptionText)
|
|
4424
|
+
: eventDescriptionText;
|
|
4425
|
+
const summarizeButtonLabel = isSummaryLoading
|
|
4426
|
+
? "Summarizing..."
|
|
4427
|
+
: shouldRenderSummary
|
|
4428
|
+
? "Summarize again"
|
|
4429
|
+
: "Summarize";
|
|
4264
4430
|
|
|
4265
4431
|
const formatDate = (date: Date) => {
|
|
4266
4432
|
return new Intl.DateTimeFormat(this.locale, {
|
|
@@ -4399,6 +4565,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4399
4565
|
this.selectedEventForDetail = null;
|
|
4400
4566
|
this.selectedEventRect = null;
|
|
4401
4567
|
this.isDescriptionExpanded = false;
|
|
4568
|
+
this.clearDescriptionSummary();
|
|
4402
4569
|
this.requestUpdate();
|
|
4403
4570
|
}}
|
|
4404
4571
|
>×</button>
|
|
@@ -4406,7 +4573,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4406
4573
|
|
|
4407
4574
|
<div class="event-detail-body">
|
|
4408
4575
|
${(() => {
|
|
4409
|
-
const teamsMatch =
|
|
4576
|
+
const teamsMatch = eventDescription?.match(/https:\/\/teams\.microsoft\.com\/[^\s<>"]+/);
|
|
4410
4577
|
const teamsUrl = teamsMatch ? teamsMatch[0] : null;
|
|
4411
4578
|
if (!event.location && !teamsUrl) return null;
|
|
4412
4579
|
return html`
|
|
@@ -4509,47 +4676,84 @@ export class CalendarViewElement extends LitElement {
|
|
|
4509
4676
|
}
|
|
4510
4677
|
|
|
4511
4678
|
${
|
|
4512
|
-
|
|
4513
|
-
(event.organizer != null &&
|
|
4514
|
-
!this.currentUserEmails.has(event.organizer.email))
|
|
4515
|
-
? event.description
|
|
4679
|
+
useDescriptionSummary
|
|
4516
4680
|
? html`
|
|
4517
4681
|
<div class="event-detail-section">
|
|
4518
4682
|
<div class="event-detail-label">Description</div>
|
|
4519
4683
|
<div class="event-detail-description ${
|
|
4520
|
-
this.isDescriptionExpanded
|
|
4684
|
+
shouldRenderSummary || this.isDescriptionExpanded
|
|
4685
|
+
? "expanded"
|
|
4686
|
+
: ""
|
|
4521
4687
|
}">
|
|
4522
|
-
<pre class="event-detail-value">${
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4688
|
+
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
4689
|
+
</div>
|
|
4690
|
+
${
|
|
4691
|
+
shouldRenderSummary && this.descriptionSummaryError
|
|
4692
|
+
? html`<div class="event-detail-value" style="opacity: 0.7;">Summary unavailable: ${this.descriptionSummaryError}. Showing original description.</div>`
|
|
4693
|
+
: null
|
|
4694
|
+
}
|
|
4695
|
+
<div class="description-actions">
|
|
4696
|
+
${
|
|
4697
|
+
!shouldRenderSummary
|
|
4698
|
+
? html`<button
|
|
4699
|
+
class="description-see-more"
|
|
4700
|
+
@click=${() => {
|
|
4701
|
+
this.isDescriptionExpanded = !this.isDescriptionExpanded;
|
|
4702
|
+
this.requestUpdate();
|
|
4703
|
+
}}
|
|
4704
|
+
>
|
|
4705
|
+
${this.isDescriptionExpanded ? "Show less" : "Show more"}
|
|
4706
|
+
</button>`
|
|
4707
|
+
: null
|
|
4708
|
+
}
|
|
4709
|
+
<button
|
|
4710
|
+
class="description-see-more"
|
|
4711
|
+
?disabled=${isSummaryLoading}
|
|
4712
|
+
@click=${() => this.requestDescriptionSummary(event)}
|
|
4713
|
+
>
|
|
4714
|
+
${summarizeButtonLabel}
|
|
4715
|
+
</button>
|
|
4716
|
+
</div>
|
|
4717
|
+
</div>
|
|
4718
|
+
`
|
|
4719
|
+
: event.readOnly ||
|
|
4720
|
+
(event.organizer != null &&
|
|
4721
|
+
!this.currentUserEmails.has(event.organizer.email))
|
|
4722
|
+
? eventDescription
|
|
4723
|
+
? html`
|
|
4724
|
+
<div class="event-detail-section">
|
|
4725
|
+
<div class="event-detail-label">Description</div>
|
|
4726
|
+
<div class="event-detail-description ${
|
|
4727
|
+
this.isDescriptionExpanded ? "expanded" : ""
|
|
4728
|
+
}">
|
|
4729
|
+
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
4526
4730
|
</div>
|
|
4527
4731
|
${
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4732
|
+
eventDescription.length > 300 ||
|
|
4733
|
+
eventDescription.split("\n").length > 8
|
|
4734
|
+
? html`
|
|
4531
4735
|
<button
|
|
4532
4736
|
class="description-see-more"
|
|
4533
4737
|
@click=${() => {
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4738
|
+
this.isDescriptionExpanded =
|
|
4739
|
+
!this.isDescriptionExpanded;
|
|
4740
|
+
this.requestUpdate();
|
|
4741
|
+
}}
|
|
4538
4742
|
>
|
|
4539
4743
|
${this.isDescriptionExpanded ? "See less" : "See more"}
|
|
4540
4744
|
</button>
|
|
4541
4745
|
`
|
|
4542
|
-
|
|
4543
|
-
|
|
4746
|
+
: null
|
|
4747
|
+
}
|
|
4544
4748
|
</div>
|
|
4545
4749
|
`
|
|
4546
|
-
|
|
4547
|
-
|
|
4750
|
+
: null
|
|
4751
|
+
: html`
|
|
4548
4752
|
<div class="event-detail-section">
|
|
4549
4753
|
<div class="event-detail-label">Description</div>
|
|
4550
4754
|
<textarea
|
|
4551
4755
|
class="event-detail-description-input"
|
|
4552
|
-
.value=${
|
|
4756
|
+
.value=${eventDescription}
|
|
4553
4757
|
placeholder="Add description..."
|
|
4554
4758
|
rows="3"
|
|
4555
4759
|
@input=${(e: Event) => {
|
|
@@ -4579,7 +4783,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4579
4783
|
clearTimeout(this.updateEventTimeout);
|
|
4580
4784
|
this.updateEventTimeout = null;
|
|
4581
4785
|
}
|
|
4582
|
-
if (newDescription !==
|
|
4786
|
+
if (newDescription !== eventDescription) {
|
|
4583
4787
|
this.dispatchEvent(
|
|
4584
4788
|
new CustomEvent("update-event", {
|
|
4585
4789
|
detail: {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes transport/tracking links commonly injected by email/calendar clients.
|
|
3
|
+
* First pass intentionally targets angle-bracket URI blobs like
|
|
4
|
+
* <https://...>, <mailto:...>, <tel:...>, <im:...>.
|
|
5
|
+
*/
|
|
6
|
+
export function sanitizeEventDescription(input: string): string {
|
|
7
|
+
if (!input) return input;
|
|
8
|
+
|
|
9
|
+
return input.replace(/<(?:https?|mailto|tel|im):[^>\s]+>/gi, "");
|
|
10
|
+
}
|