@luckydye/calendar 1.1.2 → 1.2.0
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/package.json +5 -4
- package/src/CalDAVConfig.ts +25 -0
- package/src/CalendarIntegration.ts +1 -2
- package/src/CalendarInternal.ts +20 -1
- package/src/CalendarView.ts +747 -374
- 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 +175 -63
- package/dist/calendar.js +0 -5800
package/src/CalendarView.ts
CHANGED
|
@@ -54,8 +54,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
54
54
|
display: var(--toolbar-display, flex);
|
|
55
55
|
align-items: center;
|
|
56
56
|
justify-content: space-between;
|
|
57
|
-
|
|
58
|
-
padding: 0 16px;
|
|
57
|
+
padding: 8px 10px;
|
|
59
58
|
background: var(--bg-secondary, rgba(36, 36, 38, 0.5));
|
|
60
59
|
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
61
60
|
flex-shrink: 0;
|
|
@@ -100,7 +99,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
100
99
|
background: transparent;
|
|
101
100
|
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
102
101
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
103
|
-
padding:
|
|
102
|
+
padding: 4px 8px;
|
|
104
103
|
border-radius: var(--border-radius-sm, 4px);
|
|
105
104
|
font-size: 13px;
|
|
106
105
|
line-height: 16px;
|
|
@@ -142,7 +141,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
142
141
|
background: var(--bg-input, rgba(0, 0, 0, 0.3));
|
|
143
142
|
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
144
143
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
145
|
-
padding:
|
|
144
|
+
padding: 4px 8px 4px 32px;
|
|
146
145
|
border-radius: var(--border-radius-sm, 4px);
|
|
147
146
|
font-size: 13px;
|
|
148
147
|
outline: none;
|
|
@@ -165,12 +164,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
165
164
|
gap: 8px;
|
|
166
165
|
}
|
|
167
166
|
|
|
168
|
-
.toolbar-zoom-label {
|
|
169
|
-
color: var(--text-muted, rgba(255, 255, 255, 0.6));
|
|
170
|
-
font-size: 13px;
|
|
171
|
-
white-space: nowrap;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
167
|
.toolbar-zoom-slider {
|
|
175
168
|
width: 100px;
|
|
176
169
|
height: 4px;
|
|
@@ -211,38 +204,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
211
204
|
background: var(--text-primary, rgba(255, 255, 255, 1));
|
|
212
205
|
}
|
|
213
206
|
|
|
214
|
-
.header {
|
|
215
|
-
display: flex;
|
|
216
|
-
height: 32px;
|
|
217
|
-
flex-shrink: 0;
|
|
218
|
-
padding-right: 1rem;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.weekday:nth-child(7),
|
|
222
|
-
.weekday:nth-child(6) {
|
|
223
|
-
background: var(--bg-weekend, rgba(255, 255, 255, 0.03));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.header-gutter {
|
|
227
|
-
width: 60px;
|
|
228
|
-
flex-shrink: 0;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.weekdays {
|
|
232
|
-
display: flex;
|
|
233
|
-
flex: 1;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.weekday {
|
|
237
|
-
flex: 1;
|
|
238
|
-
display: flex;
|
|
239
|
-
align-items: center;
|
|
240
|
-
justify-content: center;
|
|
241
|
-
font-size: 12px;
|
|
242
|
-
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
243
|
-
text-transform: uppercase;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
207
|
.body {
|
|
247
208
|
position: relative;
|
|
248
209
|
flex: 1;
|
|
@@ -280,6 +241,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
280
241
|
overflow-anchor: none;
|
|
281
242
|
}
|
|
282
243
|
|
|
244
|
+
:host([scroll-lock]) .scroll-container {
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
}
|
|
247
|
+
|
|
283
248
|
.scroll-container.zoom-cursor {
|
|
284
249
|
cursor: ns-resize;
|
|
285
250
|
}
|
|
@@ -369,21 +334,31 @@ export class CalendarViewElement extends LitElement {
|
|
|
369
334
|
font-size: 16px;
|
|
370
335
|
font-weight: 600;
|
|
371
336
|
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
372
|
-
background: var(--input-bg, rgba(0, 0, 0, 0.2));
|
|
373
337
|
border: 1px solid var(--input-border, rgba(255, 255, 255, 0.1));
|
|
338
|
+
background: transparent;
|
|
374
339
|
border-radius: 4px;
|
|
375
340
|
padding: 6px 8px;
|
|
376
341
|
width: 100%;
|
|
377
|
-
outline: none;
|
|
378
342
|
font-family: inherit;
|
|
379
343
|
margin: 0 -8px 0 -8px;
|
|
380
344
|
field-sizing: content;
|
|
381
345
|
resize: none;
|
|
382
346
|
}
|
|
383
347
|
|
|
348
|
+
.event-detail-description-input {
|
|
349
|
+
font-size: 14px;
|
|
350
|
+
color: inherit;
|
|
351
|
+
width: 100%;
|
|
352
|
+
padding: 0.25rem;
|
|
353
|
+
margin: -0.25rem;
|
|
354
|
+
border: none;
|
|
355
|
+
background: none;
|
|
356
|
+
field-sizing: content;
|
|
357
|
+
resize: none;
|
|
358
|
+
}
|
|
359
|
+
|
|
384
360
|
.event-detail-title-input:focus {
|
|
385
361
|
border-color: var(--input-border-focus, rgba(255, 255, 255, 0.3));
|
|
386
|
-
background: var(--input-bg-focus, rgba(0, 0, 0, 0.3));
|
|
387
362
|
}
|
|
388
363
|
|
|
389
364
|
.event-detail-time {
|
|
@@ -404,6 +379,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
404
379
|
font-size: 20px;
|
|
405
380
|
line-height: 1;
|
|
406
381
|
flex-shrink: 0;
|
|
382
|
+
position: absolute;
|
|
383
|
+
top: 0.75rem;
|
|
384
|
+
right: 1rem;
|
|
407
385
|
}
|
|
408
386
|
|
|
409
387
|
.event-detail-close:hover {
|
|
@@ -430,6 +408,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
430
408
|
text-transform: uppercase;
|
|
431
409
|
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
432
410
|
letter-spacing: 0.5px;
|
|
411
|
+
display: flex;
|
|
412
|
+
align-items: center;
|
|
413
|
+
justify-content: space-between;
|
|
433
414
|
}
|
|
434
415
|
|
|
435
416
|
.event-detail-value {
|
|
@@ -453,6 +434,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
453
434
|
.event-detail-link {
|
|
454
435
|
color: var(--accent-primary, rgb(100, 150, 255));
|
|
455
436
|
text-decoration: none;
|
|
437
|
+
white-space: nowrap;
|
|
438
|
+
overflow: hidden;
|
|
439
|
+
text-overflow: ellipsis;
|
|
440
|
+
width: 100%;
|
|
441
|
+
display: block;
|
|
456
442
|
}
|
|
457
443
|
|
|
458
444
|
.event-detail-link:hover {
|
|
@@ -535,10 +521,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
535
521
|
border: 1px solid var(--grid-color);
|
|
536
522
|
border-radius: 4px;
|
|
537
523
|
color: var(--text-primary);
|
|
538
|
-
padding:
|
|
524
|
+
padding: 4px 8px;
|
|
525
|
+
margin: -4px 0;
|
|
539
526
|
font-size: 13px;
|
|
540
527
|
cursor: pointer;
|
|
541
|
-
width: 100%;
|
|
542
528
|
}
|
|
543
529
|
|
|
544
530
|
.notification-add-button:hover {
|
|
@@ -764,19 +750,91 @@ export class CalendarViewElement extends LitElement {
|
|
|
764
750
|
return this._scrollTop;
|
|
765
751
|
}
|
|
766
752
|
|
|
767
|
-
setView(
|
|
768
|
-
|
|
769
|
-
|
|
753
|
+
setView(
|
|
754
|
+
toDayHeight: number,
|
|
755
|
+
toScrollTop: number,
|
|
756
|
+
syncScrollDOM = true,
|
|
757
|
+
animate = false,
|
|
758
|
+
): void {
|
|
759
|
+
this.cancelScrollAnimation();
|
|
760
|
+
|
|
761
|
+
if (animate) {
|
|
762
|
+
const startDayHeight = this._dayHeight;
|
|
763
|
+
const startScroll = this._scrollTop;
|
|
764
|
+
|
|
765
|
+
// Compute scroll as a fraction of total height at each zoom level for smooth interpolation
|
|
766
|
+
const startTotal = this.totalHeight;
|
|
767
|
+
|
|
768
|
+
this._dayHeight = toDayHeight;
|
|
769
|
+
this.updateWeekOffsets();
|
|
770
|
+
const targetTotal = this.totalHeight;
|
|
771
|
+
|
|
772
|
+
this._dayHeight = startDayHeight;
|
|
773
|
+
this.updateWeekOffsets();
|
|
774
|
+
|
|
775
|
+
const startFrac = startTotal > 0 ? startScroll / startTotal : 0;
|
|
776
|
+
const targetFrac = targetTotal > 0 ? toScrollTop / targetTotal : 0;
|
|
777
|
+
|
|
778
|
+
const duration = 600;
|
|
779
|
+
const startTime = performance.now();
|
|
780
|
+
const easeOutExpo = (t: number): number =>
|
|
781
|
+
t === 1 ? 1 : 1 - 2 ** (-10 * t);
|
|
782
|
+
const easeOutCubic = (t: number): number => 1 - (1 - t) ** 3;
|
|
783
|
+
|
|
784
|
+
const tick = (currentTime: number): void => {
|
|
785
|
+
if (!this.scrollAnimationFrame) return;
|
|
786
|
+
|
|
787
|
+
const progress = Math.min((currentTime - startTime) / duration, 1);
|
|
788
|
+
|
|
789
|
+
this._dayHeight = startDayHeight + (toDayHeight - startDayHeight) * easeOutExpo(progress);
|
|
790
|
+
this.updateWeekOffsets();
|
|
791
|
+
|
|
792
|
+
const currentFrac = startFrac + (targetFrac - startFrac) * easeOutCubic(progress);
|
|
793
|
+
this._scrollTop = Math.max(0, currentFrac * this.totalHeight);
|
|
794
|
+
|
|
795
|
+
if (this.scrollContainer) {
|
|
796
|
+
if (
|
|
797
|
+
this.scrollContent &&
|
|
798
|
+
this.scrollContainer.scrollHeight < this._scrollTop
|
|
799
|
+
) {
|
|
800
|
+
this.scrollContent.style.minHeight =
|
|
801
|
+
this._scrollTop + window.innerHeight + "px";
|
|
802
|
+
}
|
|
803
|
+
this.scrollContainer.scrollTop = this._scrollTop;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
this.renderCanvas();
|
|
807
|
+
|
|
808
|
+
if (progress < 1) {
|
|
809
|
+
this.scrollAnimationFrame = requestAnimationFrame(tick);
|
|
810
|
+
} else {
|
|
811
|
+
this._dayHeight = toDayHeight;
|
|
812
|
+
this._scrollTop = toScrollTop;
|
|
813
|
+
this.saveDayHeight();
|
|
814
|
+
this.saveScrollPosition();
|
|
815
|
+
this.scrollAnimationFrame = null;
|
|
816
|
+
this.renderCanvas();
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
this.scrollAnimationFrame = requestAnimationFrame(tick);
|
|
821
|
+
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
this._dayHeight = toDayHeight;
|
|
826
|
+
this._scrollTop = toScrollTop;
|
|
770
827
|
|
|
771
828
|
this.saveDayHeight();
|
|
772
829
|
this.updateWeekOffsets();
|
|
773
830
|
|
|
774
831
|
if (syncScrollDOM && this.scrollContainer && this.scrollContent) {
|
|
775
|
-
if (this.scrollContainer.scrollHeight <
|
|
776
|
-
this.scrollContent.style.minHeight =
|
|
777
|
-
|
|
832
|
+
if (this.scrollContainer.scrollHeight < toScrollTop) {
|
|
833
|
+
this.scrollContent.style.minHeight = `${
|
|
834
|
+
toScrollTop + window.innerHeight
|
|
835
|
+
}px`;
|
|
778
836
|
}
|
|
779
|
-
this.scrollContainer.scrollTop =
|
|
837
|
+
this.scrollContainer.scrollTop = toScrollTop;
|
|
780
838
|
}
|
|
781
839
|
|
|
782
840
|
this.saveScrollPosition();
|
|
@@ -846,6 +904,55 @@ export class CalendarViewElement extends LitElement {
|
|
|
846
904
|
resizingOriginalEnd: Date | null = null;
|
|
847
905
|
isResizingEvent = false;
|
|
848
906
|
|
|
907
|
+
_columnsPerRow = 7;
|
|
908
|
+
set columnsPerRow(value: number) {
|
|
909
|
+
const clamped = Math.max(1, Math.min(7, Math.floor(value)));
|
|
910
|
+
if (this._columnsPerRow !== clamped) {
|
|
911
|
+
this._columnsPerRow = clamped;
|
|
912
|
+
this.updateWeekOffsets();
|
|
913
|
+
this.renderCanvas();
|
|
914
|
+
this.requestUpdate();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
get columnsPerRow(): number {
|
|
918
|
+
return this._columnsPerRow;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
get rowsPerWeek(): number {
|
|
922
|
+
return Math.ceil(7 / this._columnsPerRow);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
getDayVisualPosition(dayIndex: number): { row: number; col: number } {
|
|
926
|
+
const row = Math.floor(dayIndex / this._columnsPerRow);
|
|
927
|
+
const col = dayIndex % this._columnsPerRow;
|
|
928
|
+
return { row, col };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
getVisualPositionFromCoords(
|
|
932
|
+
x: number,
|
|
933
|
+
y: number,
|
|
934
|
+
gridWidth: number,
|
|
935
|
+
): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
|
|
936
|
+
if (!this.scrollContainer) return null;
|
|
937
|
+
|
|
938
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
939
|
+
const col = Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth);
|
|
940
|
+
if (col < 0 || col >= this._columnsPerRow) return null;
|
|
941
|
+
|
|
942
|
+
const week = this.weeks.find(
|
|
943
|
+
(w) => w.height > 0 && y >= w.yOffset && y < w.yOffset + w.height,
|
|
944
|
+
);
|
|
945
|
+
if (!week) return null;
|
|
946
|
+
|
|
947
|
+
const rowHeight = this.dayHeight;
|
|
948
|
+
const row = Math.floor((y - week.yOffset) / rowHeight);
|
|
949
|
+
const dayIndex = row * this._columnsPerRow + col;
|
|
950
|
+
if (dayIndex < 0 || dayIndex > 6) return null;
|
|
951
|
+
|
|
952
|
+
const timeFraction = ((y - week.yOffset) % rowHeight) / rowHeight;
|
|
953
|
+
return { dayIndex, timeFraction, weekYOffset: week.yOffset };
|
|
954
|
+
}
|
|
955
|
+
|
|
849
956
|
// History stack for scroll position and zoom
|
|
850
957
|
// Index 0 is always the most recent entry
|
|
851
958
|
historyStack: Array<{ scrollTop: number; dayHeight: number }> = [];
|
|
@@ -905,8 +1012,15 @@ export class CalendarViewElement extends LitElement {
|
|
|
905
1012
|
this.internal = new CalendarInternal({
|
|
906
1013
|
locale: this.getAttribute("locale") || undefined,
|
|
907
1014
|
weekStart: Number(this.getAttribute("week-start")),
|
|
908
|
-
storage: new IndexedDBStorage()
|
|
1015
|
+
storage: new IndexedDBStorage(),
|
|
909
1016
|
});
|
|
1017
|
+
|
|
1018
|
+
this.addEventListener("wheel", this.onWheel, { passive: false });
|
|
1019
|
+
this.addEventListener("dragstart", this.onDragStart);
|
|
1020
|
+
this.addEventListener("dragend", this.onDragEnd);
|
|
1021
|
+
this.addEventListener("dragover", this.onDragOver);
|
|
1022
|
+
this.addEventListener("dragleave", this.onDragLeave);
|
|
1023
|
+
this.addEventListener("drop", this.onDrop);
|
|
910
1024
|
}
|
|
911
1025
|
|
|
912
1026
|
loadDayHeight(): number {
|
|
@@ -956,8 +1070,43 @@ export class CalendarViewElement extends LitElement {
|
|
|
956
1070
|
}
|
|
957
1071
|
}
|
|
958
1072
|
|
|
959
|
-
scrollToToday = (): void => {
|
|
960
|
-
|
|
1073
|
+
scrollToToday = (animate = true): void => {
|
|
1074
|
+
const now = new Date();
|
|
1075
|
+
|
|
1076
|
+
let weekIndex = this.weeks.findIndex((w) =>
|
|
1077
|
+
w.days.some((d) => CalendarInternal.isSameDay(d, now)),
|
|
1078
|
+
);
|
|
1079
|
+
if (weekIndex < 0) {
|
|
1080
|
+
this.weeks = this.internal.resetRangeAroundDate(now);
|
|
1081
|
+
weekIndex = this.weeks.findIndex((w) =>
|
|
1082
|
+
w.days.some((d) => CalendarInternal.isSameDay(d, now)),
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
let offsetInWeek = 0.5;
|
|
1087
|
+
if (weekIndex >= 0) {
|
|
1088
|
+
const targetWeek = this.weeks[weekIndex];
|
|
1089
|
+
const todayIndex = targetWeek.days.findIndex((d) =>
|
|
1090
|
+
CalendarInternal.isSameDay(d, now),
|
|
1091
|
+
);
|
|
1092
|
+
if (todayIndex >= 0) {
|
|
1093
|
+
const row = Math.floor(todayIndex / this._columnsPerRow);
|
|
1094
|
+
const timeFraction = (now.getHours() + now.getMinutes() / 60) / 24;
|
|
1095
|
+
offsetInWeek = (row + timeFraction) / this.rowsPerWeek;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
this.scrollToDate(now, offsetInWeek, animate, true, 900);
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
scrollToMonth = (animate = true): void => {
|
|
1103
|
+
const now = new Date();
|
|
1104
|
+
const monthMid = new Date(now.getFullYear(), now.getMonth(), 15);
|
|
1105
|
+
const dayHeight = Math.max(
|
|
1106
|
+
MIN_DAY_HEIGHT,
|
|
1107
|
+
Math.round(this.viewportHeight / 5),
|
|
1108
|
+
);
|
|
1109
|
+
this.scrollToDate(monthMid, 0.5, animate, true, dayHeight);
|
|
961
1110
|
};
|
|
962
1111
|
|
|
963
1112
|
// History management methods
|
|
@@ -1045,7 +1194,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
1045
1194
|
offsetInWeek = 0.5,
|
|
1046
1195
|
animate = false,
|
|
1047
1196
|
extendRange = false,
|
|
1197
|
+
targetDayHeight?: number,
|
|
1048
1198
|
): void {
|
|
1199
|
+
const dayHeight = targetDayHeight ?? this._dayHeight;
|
|
1200
|
+
|
|
1049
1201
|
let weekIndex = this.weeks.findIndex(
|
|
1050
1202
|
(w) =>
|
|
1051
1203
|
w.days.some((d) => CalendarInternal.isSameDay(d, targetDate)) ||
|
|
@@ -1058,7 +1210,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
1058
1210
|
// If date not found in current buffer, reset range around target (only if explicitly requested)
|
|
1059
1211
|
if (weekIndex < 0 && extendRange) {
|
|
1060
1212
|
this.weeks = this.internal.resetRangeAroundDate(targetDate);
|
|
1061
|
-
this.updateWeekOffsets();
|
|
1062
1213
|
|
|
1063
1214
|
// Find the week again in the new range
|
|
1064
1215
|
weekIndex = this.weeks.findIndex(
|
|
@@ -1072,15 +1223,24 @@ export class CalendarViewElement extends LitElement {
|
|
|
1072
1223
|
}
|
|
1073
1224
|
|
|
1074
1225
|
if (weekIndex >= 0) {
|
|
1226
|
+
// Temporarily apply target dayHeight to compute correct week offsets
|
|
1227
|
+
const savedDayHeight = this._dayHeight;
|
|
1228
|
+
this._dayHeight = dayHeight;
|
|
1229
|
+
this.updateWeekOffsets();
|
|
1230
|
+
|
|
1075
1231
|
const targetWeek = this.weeks[weekIndex];
|
|
1076
1232
|
if (targetWeek) {
|
|
1077
1233
|
const targetY = targetWeek.yOffset + targetWeek.height * offsetInWeek;
|
|
1078
1234
|
const targetScroll = Math.max(0, targetY - this.viewportHeight / 2);
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1235
|
+
|
|
1236
|
+
// Restore so animateToView reads the correct start dayHeight
|
|
1237
|
+
this._dayHeight = savedDayHeight;
|
|
1238
|
+
if (animate) this.updateWeekOffsets();
|
|
1239
|
+
|
|
1240
|
+
this.setView(dayHeight, targetScroll, true, animate);
|
|
1241
|
+
} else {
|
|
1242
|
+
this._dayHeight = savedDayHeight;
|
|
1243
|
+
this.updateWeekOffsets();
|
|
1084
1244
|
}
|
|
1085
1245
|
}
|
|
1086
1246
|
}
|
|
@@ -1092,39 +1252,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
1092
1252
|
}
|
|
1093
1253
|
};
|
|
1094
1254
|
|
|
1095
|
-
private animateScrollTo(targetScroll: number): void {
|
|
1096
|
-
this.cancelScrollAnimation();
|
|
1097
|
-
|
|
1098
|
-
if (!this.scrollContainer) return;
|
|
1099
|
-
|
|
1100
|
-
const startScroll = this.scrollTop;
|
|
1101
|
-
const distance = targetScroll - startScroll;
|
|
1102
|
-
const duration = 800;
|
|
1103
|
-
const startTime = performance.now();
|
|
1104
|
-
|
|
1105
|
-
const easeOutExpo = (t: number): number => {
|
|
1106
|
-
return t === 1 ? 1 : 1 - 2 ** (-10 * t);
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
const animate = (currentTime: number): void => {
|
|
1110
|
-
if (!this.scrollAnimationFrame) return;
|
|
1111
|
-
|
|
1112
|
-
const elapsed = currentTime - startTime;
|
|
1113
|
-
const progress = Math.min(elapsed / duration, 1);
|
|
1114
|
-
const easedProgress = easeOutExpo(progress);
|
|
1115
|
-
|
|
1116
|
-
this.scrollTop = startScroll + distance * easedProgress;
|
|
1117
|
-
|
|
1118
|
-
if (progress < 1) {
|
|
1119
|
-
this.scrollAnimationFrame = requestAnimationFrame(animate);
|
|
1120
|
-
} else {
|
|
1121
|
-
this.scrollAnimationFrame = null;
|
|
1122
|
-
}
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
this.scrollAnimationFrame = requestAnimationFrame(animate);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
1255
|
// Push current state when entering filter mode
|
|
1129
1256
|
private pushFilterToHistory(): void {
|
|
1130
1257
|
if (this.saveHistoryTimeout) {
|
|
@@ -1179,27 +1306,16 @@ export class CalendarViewElement extends LitElement {
|
|
|
1179
1306
|
|
|
1180
1307
|
window.addEventListener("mousemove", this.onMouseMove);
|
|
1181
1308
|
window.addEventListener("mouseup", this.onMouseUp);
|
|
1182
|
-
window.addEventListener("wheel", this.onWheel, { passive: false });
|
|
1183
|
-
window.addEventListener("keydown", this.onKeyDown);
|
|
1184
1309
|
window.addEventListener("paste", this.onPaste);
|
|
1185
|
-
this.addEventListener("dragstart", this.onDragStart);
|
|
1186
|
-
this.addEventListener("dragend", this.onDragEnd);
|
|
1187
|
-
this.addEventListener("dragover", this.onDragOver);
|
|
1188
|
-
this.addEventListener("dragleave", this.onDragLeave);
|
|
1189
|
-
this.addEventListener("drop", this.onDrop);
|
|
1190
1310
|
}
|
|
1191
1311
|
|
|
1192
1312
|
disconnectedCallback() {
|
|
1193
1313
|
super.disconnectedCallback();
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
window.removeEventListener("
|
|
1314
|
+
|
|
1315
|
+
window.removeEventListener("mousemove", this.onMouseMove);
|
|
1316
|
+
window.removeEventListener("mouseup", this.onMouseUp);
|
|
1197
1317
|
window.removeEventListener("paste", this.onPaste);
|
|
1198
|
-
|
|
1199
|
-
this.removeEventListener("dragend", this.onDragEnd);
|
|
1200
|
-
this.removeEventListener("dragover", this.onDragOver);
|
|
1201
|
-
this.removeEventListener("dragleave", this.onDragLeave);
|
|
1202
|
-
this.removeEventListener("drop", this.onDrop);
|
|
1318
|
+
|
|
1203
1319
|
if (this.scrollContainer) {
|
|
1204
1320
|
this.scrollContainer.removeEventListener(
|
|
1205
1321
|
"mouseleave",
|
|
@@ -1224,6 +1340,25 @@ export class CalendarViewElement extends LitElement {
|
|
|
1224
1340
|
|
|
1225
1341
|
updated() {
|
|
1226
1342
|
this.handleResize();
|
|
1343
|
+
this.repositionEventDetailOverlay();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
repositionEventDetailOverlay(): void {
|
|
1347
|
+
if (!this.selectedEventForDetail || !this.selectedEventRect) return;
|
|
1348
|
+
|
|
1349
|
+
const overlay = this.renderRoot.querySelector<HTMLElement>(".event-detail-overlay");
|
|
1350
|
+
if (!overlay) return;
|
|
1351
|
+
|
|
1352
|
+
const actualHeight = overlay.offsetHeight;
|
|
1353
|
+
const GAP = 8;
|
|
1354
|
+
const containerHeight = this.scrollContainer?.clientHeight || 600;
|
|
1355
|
+
|
|
1356
|
+
const rawTop = this.selectedEventRect.y - this.scrollTop;
|
|
1357
|
+
const minTop = GAP;
|
|
1358
|
+
const maxTop = containerHeight - actualHeight - GAP;
|
|
1359
|
+
const top = Math.max(minTop, Math.min(maxTop, rawTop));
|
|
1360
|
+
|
|
1361
|
+
overlay.style.top = `${top}px`;
|
|
1227
1362
|
}
|
|
1228
1363
|
|
|
1229
1364
|
async firstUpdated() {
|
|
@@ -1246,7 +1381,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1246
1381
|
}
|
|
1247
1382
|
|
|
1248
1383
|
if (this.scrollContainer) {
|
|
1249
|
-
this.scrollContainer.addEventListener("scroll", this.onScroll
|
|
1384
|
+
this.scrollContainer.addEventListener("scroll", this.onScroll, {
|
|
1385
|
+
passive: false,
|
|
1386
|
+
});
|
|
1250
1387
|
this.scrollContainer.addEventListener(
|
|
1251
1388
|
"mouseleave",
|
|
1252
1389
|
this.onScrollContainerMouseLeave,
|
|
@@ -1323,6 +1460,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1323
1460
|
|
|
1324
1461
|
updateWeekOffsets(): void {
|
|
1325
1462
|
let y = 0;
|
|
1463
|
+
const weekHeight = this.dayHeight * this.rowsPerWeek;
|
|
1326
1464
|
|
|
1327
1465
|
if (this.filter) {
|
|
1328
1466
|
const filteredEvents = this.events;
|
|
@@ -1345,13 +1483,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
1345
1483
|
(range) => range.end >= weekStartTime && range.start <= weekEndTime,
|
|
1346
1484
|
);
|
|
1347
1485
|
|
|
1348
|
-
week.height = hasEvents ?
|
|
1486
|
+
week.height = hasEvents ? weekHeight : 0;
|
|
1349
1487
|
y += week.height;
|
|
1350
1488
|
}
|
|
1351
1489
|
} else {
|
|
1352
1490
|
for (const week of this.weeks) {
|
|
1353
1491
|
week.yOffset = y;
|
|
1354
|
-
week.height =
|
|
1492
|
+
week.height = weekHeight;
|
|
1355
1493
|
y += week.height;
|
|
1356
1494
|
}
|
|
1357
1495
|
}
|
|
@@ -1391,21 +1529,43 @@ export class CalendarViewElement extends LitElement {
|
|
|
1391
1529
|
}
|
|
1392
1530
|
}
|
|
1393
1531
|
|
|
1532
|
+
this.updateColumnsForViewport();
|
|
1394
1533
|
this.renderCanvas();
|
|
1395
1534
|
}
|
|
1396
1535
|
|
|
1536
|
+
updateColumnsForViewport(): void {
|
|
1537
|
+
if (!this.scrollContainer) return;
|
|
1538
|
+
|
|
1539
|
+
const rect = this.scrollContainer.getBoundingClientRect();
|
|
1540
|
+
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
1541
|
+
|
|
1542
|
+
// Determine optimal columns based on available width
|
|
1543
|
+
// Minimum 60px per day column for usability
|
|
1544
|
+
const minDayWidth = 120;
|
|
1545
|
+
const optimalColumns = Math.max(
|
|
1546
|
+
1,
|
|
1547
|
+
Math.min(7, Math.floor(gridWidth / minDayWidth)),
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
if (this._columnsPerRow !== optimalColumns) {
|
|
1551
|
+
this._columnsPerRow = optimalColumns;
|
|
1552
|
+
this.updateWeekOffsets();
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1397
1556
|
renderCanvas(): void {
|
|
1398
1557
|
if (!this.ctx || !this.canvas || !this.scrollContainer) return;
|
|
1399
1558
|
|
|
1400
1559
|
const ctx = this.ctx;
|
|
1401
1560
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
1402
1561
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
1562
|
+
const fontFamily = getComputedStyle(this).fontFamily;
|
|
1403
1563
|
|
|
1404
1564
|
ctx.clearRect(0, 0, width, height);
|
|
1405
1565
|
|
|
1406
1566
|
const scrollTop = this.scrollTop;
|
|
1407
1567
|
const gridWidth = width - LEFT_GUTTER_WIDTH;
|
|
1408
|
-
const dayWidth = gridWidth /
|
|
1568
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
1409
1569
|
const today = new Date();
|
|
1410
1570
|
|
|
1411
1571
|
// Find visible weeks
|
|
@@ -1423,19 +1583,20 @@ export class CalendarViewElement extends LitElement {
|
|
|
1423
1583
|
CalendarInternal.isSameDay(d, today),
|
|
1424
1584
|
);
|
|
1425
1585
|
if (todayIndex >= 0) {
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1586
|
+
const { row, col } = this.getDayVisualPosition(todayIndex);
|
|
1587
|
+
const x = LEFT_GUTTER_WIDTH + col * dayWidth;
|
|
1588
|
+
const dayY = week.yOffset + row * this.dayHeight - scrollTop;
|
|
1428
1589
|
const bgToday =
|
|
1429
1590
|
getComputedStyle(this).getPropertyValue("--bg-today").trim() ||
|
|
1430
1591
|
"rgba(255, 255, 255, 0.05)";
|
|
1431
1592
|
ctx.fillStyle = bgToday;
|
|
1432
|
-
ctx.fillRect(x,
|
|
1593
|
+
ctx.fillRect(x, dayY, dayWidth, this.dayHeight);
|
|
1433
1594
|
|
|
1434
1595
|
if (showTimeScale) {
|
|
1435
1596
|
// Draw current time indicator line (zoomed in)
|
|
1436
1597
|
const now = new Date();
|
|
1437
1598
|
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
1438
|
-
const timeY =
|
|
1599
|
+
const timeY = dayY + (currentMinutes / 1440) * this.dayHeight;
|
|
1439
1600
|
if (timeY >= 0 && timeY <= height) {
|
|
1440
1601
|
const accentTime =
|
|
1441
1602
|
getComputedStyle(this)
|
|
@@ -1459,8 +1620,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
1459
1620
|
"rgba(255, 255, 255, 0.1)";
|
|
1460
1621
|
ctx.lineWidth = 1;
|
|
1461
1622
|
|
|
1462
|
-
// Vertical lines (day separators)
|
|
1463
|
-
for (let i = 1; i <=
|
|
1623
|
+
// Vertical lines (day separators) - draw for each column
|
|
1624
|
+
for (let i = 1; i <= this._columnsPerRow; i++) {
|
|
1464
1625
|
const x = LEFT_GUTTER_WIDTH + i * dayWidth;
|
|
1465
1626
|
ctx.beginPath();
|
|
1466
1627
|
ctx.moveTo(x, 0);
|
|
@@ -1541,15 +1702,28 @@ export class CalendarViewElement extends LitElement {
|
|
|
1541
1702
|
ctx.lineTo(width, y);
|
|
1542
1703
|
ctx.stroke();
|
|
1543
1704
|
|
|
1705
|
+
// Draw horizontal lines between visual rows within this week
|
|
1706
|
+
for (let row = 1; row < this.rowsPerWeek; row++) {
|
|
1707
|
+
const rowY = y + row * this.dayHeight;
|
|
1708
|
+
if (rowY >= 0 && rowY <= height) {
|
|
1709
|
+
ctx.beginPath();
|
|
1710
|
+
ctx.moveTo(LEFT_GUTTER_WIDTH, rowY);
|
|
1711
|
+
ctx.lineTo(width, rowY);
|
|
1712
|
+
ctx.stroke();
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1544
1716
|
// Left gutter: week number and time scale
|
|
1545
1717
|
const hourLabelOpacity = Math.max(
|
|
1546
1718
|
0,
|
|
1547
1719
|
Math.min(1, (this.dayHeight - 300) / 300),
|
|
1548
1720
|
);
|
|
1549
1721
|
|
|
1722
|
+
ctx.font = `500 11px ${fontFamily}`;
|
|
1723
|
+
ctx.textBaseline = "bottom";
|
|
1550
1724
|
ctx.textAlign = "right";
|
|
1551
1725
|
|
|
1552
|
-
// Draw hourly lines and labels
|
|
1726
|
+
// Draw hourly lines and labels for each visual row
|
|
1553
1727
|
const gridColor =
|
|
1554
1728
|
getComputedStyle(this).getPropertyValue("--grid-color").trim() ||
|
|
1555
1729
|
"rgba(255, 255, 255, 0.1)";
|
|
@@ -1568,19 +1742,22 @@ export class CalendarViewElement extends LitElement {
|
|
|
1568
1742
|
`${0.4 * hourLabelOpacity})`,
|
|
1569
1743
|
);
|
|
1570
1744
|
|
|
1571
|
-
for (let
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1745
|
+
for (let row = 0; row < this.rowsPerWeek; row++) {
|
|
1746
|
+
const rowY = y + row * this.dayHeight;
|
|
1747
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
1748
|
+
const hourY = rowY + (hour / 24) * this.dayHeight;
|
|
1749
|
+
if (hourY >= 0 && hourY <= height) {
|
|
1750
|
+
// Hour line
|
|
1751
|
+
ctx.beginPath();
|
|
1752
|
+
ctx.moveTo(LEFT_GUTTER_WIDTH, hourY);
|
|
1753
|
+
ctx.lineTo(width, hourY);
|
|
1754
|
+
ctx.stroke();
|
|
1579
1755
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1756
|
+
// Hour label (only draw if opacity is significant)
|
|
1757
|
+
if (hourLabelOpacity > 0.1) {
|
|
1758
|
+
const label = `${hour.toString().padStart(2, "0")}:00`;
|
|
1759
|
+
ctx.fillText(label, 48, hourY + 4);
|
|
1760
|
+
}
|
|
1584
1761
|
}
|
|
1585
1762
|
}
|
|
1586
1763
|
}
|
|
@@ -1588,13 +1765,15 @@ export class CalendarViewElement extends LitElement {
|
|
|
1588
1765
|
// Draw current time indicator in left gutter (only when timescale is visible and today is in this week)
|
|
1589
1766
|
if (hourLabelOpacity > 0.1) {
|
|
1590
1767
|
const today = new Date();
|
|
1591
|
-
const
|
|
1768
|
+
const todayIndex = week.days.findIndex((d) =>
|
|
1592
1769
|
CalendarInternal.isSameDay(d, today),
|
|
1593
1770
|
);
|
|
1594
1771
|
|
|
1595
|
-
if (
|
|
1772
|
+
if (todayIndex >= 0) {
|
|
1773
|
+
const { row } = this.getDayVisualPosition(todayIndex);
|
|
1596
1774
|
const currentMinutes = today.getHours() * 60 + today.getMinutes();
|
|
1597
|
-
const timeY =
|
|
1775
|
+
const timeY =
|
|
1776
|
+
y + row * this.dayHeight + (currentMinutes / 1440) * this.dayHeight;
|
|
1598
1777
|
|
|
1599
1778
|
if (timeY >= 0 && timeY <= height) {
|
|
1600
1779
|
const hours = today.getHours().toString().padStart(2, "0");
|
|
@@ -1627,7 +1806,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
1627
1806
|
ctx.fill();
|
|
1628
1807
|
|
|
1629
1808
|
// Draw current time text in white
|
|
1630
|
-
ctx.fillStyle =
|
|
1809
|
+
ctx.fillStyle =
|
|
1810
|
+
getComputedStyle(this)
|
|
1811
|
+
.getPropertyValue("--text-primary")
|
|
1812
|
+
.trim() || "rgba(255, 255, 255, 1)";
|
|
1631
1813
|
ctx.fillText(timeText, textX, textY);
|
|
1632
1814
|
ctx.restore();
|
|
1633
1815
|
}
|
|
@@ -1671,6 +1853,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
1671
1853
|
this.renderEventsOnCanvas(ctx, scrollTop, height, dayWidth, visibleWeeks);
|
|
1672
1854
|
|
|
1673
1855
|
this.renderDateLabels();
|
|
1856
|
+
|
|
1857
|
+
// Draw sticky weekday labels at top of viewport
|
|
1858
|
+
this.renderWeekdayLabels(ctx, dayWidth, visibleWeeks, scrollTop, height);
|
|
1859
|
+
|
|
1674
1860
|
if (this.isCreatingEvent) {
|
|
1675
1861
|
this.renderEventCreationPreview();
|
|
1676
1862
|
}
|
|
@@ -1800,7 +1986,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1800
1986
|
});
|
|
1801
1987
|
}
|
|
1802
1988
|
return result;
|
|
1803
|
-
|
|
1989
|
+
})
|
|
1804
1990
|
: timedSegments;
|
|
1805
1991
|
|
|
1806
1992
|
// Combine segments with all-day events first (so they get top rows)
|
|
@@ -1946,11 +2132,21 @@ export class CalendarViewElement extends LitElement {
|
|
|
1946
2132
|
isEnd,
|
|
1947
2133
|
totalWeeks,
|
|
1948
2134
|
} = segment;
|
|
1949
|
-
const weekHeight = week.height;
|
|
1950
2135
|
const weekYOffset = week.yOffset;
|
|
1951
2136
|
|
|
1952
2137
|
const allDay = event.isAllDay === true;
|
|
1953
2138
|
|
|
2139
|
+
// Get visual position for the start day
|
|
2140
|
+
const startVisualPos = this.getDayVisualPosition(startDayIndex);
|
|
2141
|
+
const endVisualPos = this.getDayVisualPosition(endDayIndex);
|
|
2142
|
+
|
|
2143
|
+
// Skip segments that span multiple visual rows for now (would need to be split)
|
|
2144
|
+
// For simple case, render each day on its visual row
|
|
2145
|
+
if (startVisualPos.row !== endVisualPos.row) {
|
|
2146
|
+
// Event spans visual rows - for now, only render the start portion
|
|
2147
|
+
// TODO: Split into multiple segments across visual rows
|
|
2148
|
+
}
|
|
2149
|
+
|
|
1954
2150
|
let yStart: number;
|
|
1955
2151
|
let yEnd: number;
|
|
1956
2152
|
|
|
@@ -1981,8 +2177,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
1981
2177
|
const endMinutes =
|
|
1982
2178
|
effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes();
|
|
1983
2179
|
|
|
1984
|
-
|
|
1985
|
-
|
|
2180
|
+
// Position within the visual row
|
|
2181
|
+
const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
|
|
2182
|
+
yStart = visualRowY + (startMinutes / 1440) * this.dayHeight;
|
|
2183
|
+
yEnd = visualRowY + (endMinutes / 1440) * this.dayHeight;
|
|
1986
2184
|
} else {
|
|
1987
2185
|
const eventKey = `${weekIndex}-${event.id}`;
|
|
1988
2186
|
let rowIndex = eventRowIndex.get(eventKey);
|
|
@@ -2015,12 +2213,14 @@ export class CalendarViewElement extends LitElement {
|
|
|
2015
2213
|
occupied.add(rowIndex);
|
|
2016
2214
|
}
|
|
2017
2215
|
|
|
2018
|
-
const
|
|
2019
|
-
(
|
|
2216
|
+
const maxEventsInRow = Math.floor(
|
|
2217
|
+
(this.dayHeight - 4) / (MIN_EVENT_HEIGHT + 2),
|
|
2020
2218
|
);
|
|
2021
|
-
if (rowIndex >=
|
|
2219
|
+
if (rowIndex >= maxEventsInRow) continue;
|
|
2022
2220
|
|
|
2023
|
-
|
|
2221
|
+
// Position within the visual row
|
|
2222
|
+
const visualRowY = weekYOffset + startVisualPos.row * this.dayHeight;
|
|
2223
|
+
yStart = visualRowY + 4 + rowIndex * (MIN_EVENT_HEIGHT + 2);
|
|
2024
2224
|
yEnd = yStart + MIN_EVENT_HEIGHT;
|
|
2025
2225
|
}
|
|
2026
2226
|
|
|
@@ -2046,13 +2246,15 @@ export class CalendarViewElement extends LitElement {
|
|
|
2046
2246
|
const columnWidth = dayWidth / columnLayout.totalColumns;
|
|
2047
2247
|
x =
|
|
2048
2248
|
LEFT_GUTTER_WIDTH +
|
|
2049
|
-
|
|
2249
|
+
startVisualPos.col * dayWidth +
|
|
2050
2250
|
columnLayout.column * columnWidth;
|
|
2051
2251
|
spanWidth = columnWidth;
|
|
2052
2252
|
} else {
|
|
2053
|
-
// Normal layout
|
|
2054
|
-
|
|
2055
|
-
|
|
2253
|
+
// Normal layout - use visual column position
|
|
2254
|
+
// For multi-day events on same visual row, calculate span
|
|
2255
|
+
const colSpan = endVisualPos.col - startVisualPos.col + 1;
|
|
2256
|
+
x = LEFT_GUTTER_WIDTH + startVisualPos.col * dayWidth;
|
|
2257
|
+
spanWidth = colSpan * dayWidth;
|
|
2056
2258
|
}
|
|
2057
2259
|
|
|
2058
2260
|
// Convert to viewport coordinates
|
|
@@ -2424,7 +2626,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2424
2626
|
const stickyTop = Math.max(0, scrollTop - labelY);
|
|
2425
2627
|
const maxStickyTop = nextMonthY - labelY - 24;
|
|
2426
2628
|
const clampedStickyTop = Math.min(stickyTop, maxStickyTop);
|
|
2427
|
-
const labelTopMargin =
|
|
2629
|
+
const labelTopMargin = 32;
|
|
2428
2630
|
const finalTop = labelY + clampedStickyTop - scrollTop + labelTopMargin;
|
|
2429
2631
|
|
|
2430
2632
|
ctx.save();
|
|
@@ -2435,7 +2637,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2435
2637
|
const labelText = `${month.monthName} ${month.year}`;
|
|
2436
2638
|
const textWidth = ctx.measureText(labelText).width;
|
|
2437
2639
|
const leftMargin = 8;
|
|
2438
|
-
const textX =
|
|
2640
|
+
const textX = 64 + padding[3] + leftMargin;
|
|
2439
2641
|
const textY = finalTop + padding[0];
|
|
2440
2642
|
|
|
2441
2643
|
// Draw background
|
|
@@ -2466,6 +2668,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
2466
2668
|
}
|
|
2467
2669
|
|
|
2468
2670
|
onWheel = (e: WheelEvent): void => {
|
|
2671
|
+
if (this.hasAttribute("scroll-lock")) return;
|
|
2672
|
+
|
|
2469
2673
|
if (this.scrollAnimationFrame) {
|
|
2470
2674
|
this.cancelScrollAnimation();
|
|
2471
2675
|
}
|
|
@@ -2953,110 +3157,84 @@ export class CalendarViewElement extends LitElement {
|
|
|
2953
3157
|
}
|
|
2954
3158
|
};
|
|
2955
3159
|
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
this.dispatchEvent(
|
|
2960
|
-
new CustomEvent("force-sync", {
|
|
2961
|
-
bubbles: true,
|
|
2962
|
-
}),
|
|
2963
|
-
);
|
|
2964
|
-
} else if ((e.metaKey || e.ctrlKey) && e.key === "c") {
|
|
2965
|
-
// Don't interfere with copying text from input fields
|
|
2966
|
-
const target = e.target as HTMLElement;
|
|
2967
|
-
if (target.matches('input, textarea, [contenteditable="true"]')) {
|
|
2968
|
-
return;
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
const selectedEvents = this.internal.getSelectedEvents();
|
|
2972
|
-
if (selectedEvents.length === 0) return;
|
|
2973
|
-
|
|
2974
|
-
e.preventDefault();
|
|
3160
|
+
forceSync(): void {
|
|
3161
|
+
this.dispatchEvent(new CustomEvent("force-sync", { bubbles: true }));
|
|
3162
|
+
}
|
|
2975
3163
|
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
}
|
|
3164
|
+
copySelectedEvents(): void {
|
|
3165
|
+
const selectedEvents = this.internal.getSelectedEvents();
|
|
3166
|
+
if (selectedEvents.length === 0) return;
|
|
3167
|
+
|
|
3168
|
+
const icalText = serializeEventsToICal(selectedEvents);
|
|
3169
|
+
const blob = new Blob([icalText], { type: "text/plain" });
|
|
3170
|
+
const item = new ClipboardItem({ "text/plain": blob });
|
|
3171
|
+
navigator.clipboard.write([item]).then(() => {
|
|
3172
|
+
const count = selectedEvents.length;
|
|
3173
|
+
queueStatus(
|
|
3174
|
+
`Copied ${count} event${count === 1 ? "" : "s"} to clipboard`,
|
|
3175
|
+
);
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2991
3178
|
|
|
2992
|
-
|
|
2993
|
-
|
|
3179
|
+
deleteSelectedEvents(): void {
|
|
3180
|
+
const selectedEvents = this.internal.getSelectedEvents();
|
|
3181
|
+
if (selectedEvents.length === 0) return;
|
|
2994
3182
|
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
if (deletableEvents.length === 0) return;
|
|
3183
|
+
const deletableEvents = selectedEvents.filter((event) => !event.readOnly);
|
|
3184
|
+
if (deletableEvents.length === 0) return;
|
|
2998
3185
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3186
|
+
this.dispatchEvent(
|
|
3187
|
+
new CustomEvent("delete-events", {
|
|
3188
|
+
detail: { events: deletableEvents },
|
|
3189
|
+
bubbles: true,
|
|
3190
|
+
}),
|
|
3191
|
+
);
|
|
3001
3192
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3193
|
+
this.internal.clearSelection();
|
|
3194
|
+
this.selectedEventForDetail = null;
|
|
3195
|
+
this.selectedEventRect = null;
|
|
3196
|
+
this.dispatchEvent(
|
|
3197
|
+
new CustomEvent("selection-change", {
|
|
3198
|
+
detail: { selectedEvents: [] },
|
|
3199
|
+
bubbles: true,
|
|
3200
|
+
}),
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3008
3203
|
|
|
3009
|
-
|
|
3204
|
+
escape(): void {
|
|
3205
|
+
if (this.isCreatingEvent || this.eventCreationStart) {
|
|
3206
|
+
this.clearEventCreationState();
|
|
3207
|
+
this.renderCanvas();
|
|
3208
|
+
queueStatus("Event creation cancelled");
|
|
3209
|
+
} else if (this.isDraggingEvent || this.movingEvent) {
|
|
3210
|
+
this.resetDragState();
|
|
3211
|
+
this.renderCanvas();
|
|
3212
|
+
queueStatus("Event move cancelled");
|
|
3213
|
+
} else if (this.isResizingEvent) {
|
|
3214
|
+
this.isResizingEvent = false;
|
|
3215
|
+
this.resizeEvent = null;
|
|
3216
|
+
this.resizeStartY = 0;
|
|
3217
|
+
this.resizeEdge = null;
|
|
3218
|
+
this.renderCanvas();
|
|
3219
|
+
queueStatus("Event resize cancelled");
|
|
3220
|
+
} else if (
|
|
3221
|
+
this.internal.getSelectedEvents().length > 0 ||
|
|
3222
|
+
this.selectedEventForDetail
|
|
3223
|
+
) {
|
|
3010
3224
|
this.internal.clearSelection();
|
|
3011
3225
|
this.selectedEventForDetail = null;
|
|
3012
3226
|
this.selectedEventRect = null;
|
|
3227
|
+
this.isDescriptionExpanded = false;
|
|
3228
|
+
this.renderCanvas();
|
|
3229
|
+
this.requestUpdate();
|
|
3013
3230
|
this.dispatchEvent(
|
|
3014
3231
|
new CustomEvent("selection-change", {
|
|
3015
3232
|
detail: { selectedEvents: [] },
|
|
3016
3233
|
bubbles: true,
|
|
3017
3234
|
}),
|
|
3018
3235
|
);
|
|
3019
|
-
} else if (e.key === "Escape") {
|
|
3020
|
-
// Cancel any active dragging/creating operations
|
|
3021
|
-
if (this.isCreatingEvent || this.eventCreationStart) {
|
|
3022
|
-
e.preventDefault();
|
|
3023
|
-
this.clearEventCreationState();
|
|
3024
|
-
this.renderCanvas();
|
|
3025
|
-
queueStatus("Event creation cancelled");
|
|
3026
|
-
} else if (this.isDraggingEvent || this.movingEvent) {
|
|
3027
|
-
e.preventDefault();
|
|
3028
|
-
this.resetDragState();
|
|
3029
|
-
this.renderCanvas();
|
|
3030
|
-
queueStatus("Event move cancelled");
|
|
3031
|
-
} else if (this.isResizingEvent) {
|
|
3032
|
-
e.preventDefault();
|
|
3033
|
-
this.isResizingEvent = false;
|
|
3034
|
-
this.resizeEvent = null;
|
|
3035
|
-
this.resizeStartY = 0;
|
|
3036
|
-
this.resizeEdge = null;
|
|
3037
|
-
this.renderCanvas();
|
|
3038
|
-
queueStatus("Event resize cancelled");
|
|
3039
|
-
} else if (
|
|
3040
|
-
this.internal.getSelectedEvents().length > 0 ||
|
|
3041
|
-
this.selectedEventForDetail
|
|
3042
|
-
) {
|
|
3043
|
-
// Clear selection if there are selected events or detail overlay is open
|
|
3044
|
-
e.preventDefault();
|
|
3045
|
-
this.internal.clearSelection();
|
|
3046
|
-
this.selectedEventForDetail = null;
|
|
3047
|
-
this.selectedEventRect = null;
|
|
3048
|
-
this.isDescriptionExpanded = false;
|
|
3049
|
-
this.renderCanvas();
|
|
3050
|
-
this.requestUpdate();
|
|
3051
|
-
this.dispatchEvent(
|
|
3052
|
-
new CustomEvent("selection-change", {
|
|
3053
|
-
detail: { selectedEvents: [] },
|
|
3054
|
-
bubbles: true,
|
|
3055
|
-
}),
|
|
3056
|
-
);
|
|
3057
|
-
}
|
|
3058
3236
|
}
|
|
3059
|
-
}
|
|
3237
|
+
}
|
|
3060
3238
|
|
|
3061
3239
|
onScrollContainerMouseMove = (e: MouseEvent): void => {
|
|
3062
3240
|
if (!this.scrollContainer || this.isDraggingZoom) return;
|
|
@@ -3506,16 +3684,22 @@ export class CalendarViewElement extends LitElement {
|
|
|
3506
3684
|
|
|
3507
3685
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
3508
3686
|
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
3509
|
-
const dayWidth = gridWidth /
|
|
3687
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
3510
3688
|
|
|
3511
|
-
// Find
|
|
3512
|
-
const
|
|
3689
|
+
// Find column indices from X coordinates
|
|
3690
|
+
const startCol = Math.max(
|
|
3513
3691
|
0,
|
|
3514
|
-
Math.min(
|
|
3692
|
+
Math.min(
|
|
3693
|
+
this._columnsPerRow - 1,
|
|
3694
|
+
Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
3695
|
+
),
|
|
3515
3696
|
);
|
|
3516
|
-
const
|
|
3697
|
+
const endCol = Math.max(
|
|
3517
3698
|
0,
|
|
3518
|
-
Math.min(
|
|
3699
|
+
Math.min(
|
|
3700
|
+
this._columnsPerRow - 1,
|
|
3701
|
+
Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
3702
|
+
),
|
|
3519
3703
|
);
|
|
3520
3704
|
|
|
3521
3705
|
// Find all weeks that intersect with the selection box
|
|
@@ -3533,29 +3717,39 @@ export class CalendarViewElement extends LitElement {
|
|
|
3533
3717
|
// Example: Wed 14:00-16:00 across 3 weeks = 3 separate time ranges
|
|
3534
3718
|
|
|
3535
3719
|
for (const week of intersectingWeeks) {
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
// For each day in the selection
|
|
3540
|
-
for (
|
|
3541
|
-
let dayIndex = startDayIndex;
|
|
3542
|
-
dayIndex <= endDayIndex;
|
|
3543
|
-
dayIndex++
|
|
3544
|
-
) {
|
|
3720
|
+
// Check each day to see if it's in the selection box
|
|
3721
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
3545
3722
|
const day = week.days[dayIndex];
|
|
3546
3723
|
if (!day) continue;
|
|
3547
3724
|
|
|
3725
|
+
const { row, col } = this.getDayVisualPosition(dayIndex);
|
|
3726
|
+
|
|
3727
|
+
// Check if this day's column is in the selection
|
|
3728
|
+
if (col < startCol || col > endCol) continue;
|
|
3729
|
+
|
|
3730
|
+
// Calculate Y bounds for this visual row
|
|
3731
|
+
const rowTop = week.yOffset + row * this.dayHeight;
|
|
3732
|
+
const rowBottom = rowTop + this.dayHeight;
|
|
3733
|
+
|
|
3734
|
+
// Check if selection intersects with this row
|
|
3735
|
+
if (maxY < rowTop || minY > rowBottom) continue;
|
|
3736
|
+
|
|
3737
|
+
const dayMinY = Math.max(minY, rowTop);
|
|
3738
|
+
const dayMaxY = Math.min(maxY, rowBottom);
|
|
3739
|
+
|
|
3548
3740
|
// Calculate start time for this day
|
|
3549
|
-
const dayStartOffset =
|
|
3741
|
+
const dayStartOffset = dayMinY - rowTop;
|
|
3550
3742
|
const startMinutes = Math.floor(
|
|
3551
|
-
(dayStartOffset /
|
|
3743
|
+
(dayStartOffset / this.dayHeight) * 24 * 60,
|
|
3552
3744
|
);
|
|
3553
3745
|
const startHour = Math.floor(startMinutes / 60);
|
|
3554
3746
|
const startMinute = startMinutes % 60;
|
|
3555
3747
|
|
|
3556
3748
|
// Calculate end time for this day
|
|
3557
|
-
const dayEndOffset =
|
|
3558
|
-
const endMinutes = Math.ceil(
|
|
3749
|
+
const dayEndOffset = dayMaxY - rowTop;
|
|
3750
|
+
const endMinutes = Math.ceil(
|
|
3751
|
+
(dayEndOffset / this.dayHeight) * 24 * 60,
|
|
3752
|
+
);
|
|
3559
3753
|
const endHour = Math.floor(endMinutes / 60);
|
|
3560
3754
|
const endMinute = endMinutes % 60;
|
|
3561
3755
|
|
|
@@ -3569,21 +3763,33 @@ export class CalendarViewElement extends LitElement {
|
|
|
3569
3763
|
}
|
|
3570
3764
|
}
|
|
3571
3765
|
} else {
|
|
3572
|
-
// ZOOMED OUT: Create time ranges per
|
|
3573
|
-
//
|
|
3766
|
+
// ZOOMED OUT: Create time ranges per visible day in selection
|
|
3767
|
+
// For each day whose visual position is in the selection box
|
|
3574
3768
|
|
|
3575
3769
|
for (const week of intersectingWeeks) {
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3770
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
3771
|
+
const day = week.days[dayIndex];
|
|
3772
|
+
if (!day) continue;
|
|
3579
3773
|
|
|
3580
|
-
|
|
3581
|
-
rangeStart.setHours(0, 0, 0, 0);
|
|
3774
|
+
const { row, col } = this.getDayVisualPosition(dayIndex);
|
|
3582
3775
|
|
|
3583
|
-
|
|
3584
|
-
|
|
3776
|
+
// Check if this day's column is in the selection
|
|
3777
|
+
if (col < startCol || col > endCol) continue;
|
|
3585
3778
|
|
|
3586
|
-
|
|
3779
|
+
// Check if this day's row is in the selection
|
|
3780
|
+
const rowTop = week.yOffset + row * this.dayHeight;
|
|
3781
|
+
const rowBottom = rowTop + this.dayHeight;
|
|
3782
|
+
|
|
3783
|
+
if (maxY < rowTop || minY > rowBottom) continue;
|
|
3784
|
+
|
|
3785
|
+
const rangeStart = new Date(day);
|
|
3786
|
+
rangeStart.setHours(0, 0, 0, 0);
|
|
3787
|
+
|
|
3788
|
+
const rangeEnd = new Date(day);
|
|
3789
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
3790
|
+
|
|
3791
|
+
timeRanges.push({ start: rangeStart, end: rangeEnd });
|
|
3792
|
+
}
|
|
3587
3793
|
}
|
|
3588
3794
|
}
|
|
3589
3795
|
|
|
@@ -3661,6 +3867,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
3661
3867
|
|
|
3662
3868
|
this.renderCanvas();
|
|
3663
3869
|
|
|
3870
|
+
this.repositionEventDetailOverlay();
|
|
3871
|
+
|
|
3664
3872
|
this.checkAndExtendRange();
|
|
3665
3873
|
|
|
3666
3874
|
this.saveScrollPosition();
|
|
@@ -3761,14 +3969,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
3761
3969
|
this.internal.setFilter(input.value);
|
|
3762
3970
|
};
|
|
3763
3971
|
|
|
3764
|
-
onCalDAVClick = (): void => {
|
|
3765
|
-
this.dispatchEvent(new CustomEvent("caldav-config", { bubbles: true }));
|
|
3766
|
-
};
|
|
3767
|
-
|
|
3768
3972
|
onNotificationPopoverToggle = async (): Promise<void> => {
|
|
3769
3973
|
this.notificationPopoverOpen = !this.notificationPopoverOpen;
|
|
3770
3974
|
if (this.notificationPopoverOpen) {
|
|
3771
|
-
this.dispatchEvent(
|
|
3975
|
+
this.dispatchEvent(
|
|
3976
|
+
new CustomEvent("load-notifications", { bubbles: true }),
|
|
3977
|
+
);
|
|
3772
3978
|
}
|
|
3773
3979
|
this.requestUpdate();
|
|
3774
3980
|
};
|
|
@@ -3778,7 +3984,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
3778
3984
|
this.requestUpdate();
|
|
3779
3985
|
};
|
|
3780
3986
|
|
|
3781
|
-
|
|
3782
3987
|
onThemeChange = (e: Event): void => {
|
|
3783
3988
|
const select = e.target as HTMLSelectElement;
|
|
3784
3989
|
const theme = select.value as ThemeName;
|
|
@@ -3813,8 +4018,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
3813
4018
|
async onEventClick(event: CalendarEvent, e: MouseEvent): Promise<void> {
|
|
3814
4019
|
const isCmdOrCtrl = e.metaKey || e.ctrlKey;
|
|
3815
4020
|
|
|
3816
|
-
console.log(event);
|
|
3817
|
-
|
|
3818
4021
|
if (isCmdOrCtrl) {
|
|
3819
4022
|
this.internal.selectEvent(event, "toggle");
|
|
3820
4023
|
} else {
|
|
@@ -4091,17 +4294,21 @@ export class CalendarViewElement extends LitElement {
|
|
|
4091
4294
|
const fontFamily = getComputedStyle(this).fontFamily;
|
|
4092
4295
|
const scrollRect = this.scrollContainer.getBoundingClientRect();
|
|
4093
4296
|
const gridWidth = scrollRect.width - LEFT_GUTTER_WIDTH - MINIMAP_WIDTH;
|
|
4094
|
-
const dayWidth = gridWidth /
|
|
4297
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4095
4298
|
|
|
4096
4299
|
const getTimeY = (day: Date, hours: number, minutes: number) => {
|
|
4097
4300
|
const week = this.weeks.find((w) =>
|
|
4098
4301
|
w.days.some((d) => d.toDateString() === day.toDateString()),
|
|
4099
4302
|
);
|
|
4100
4303
|
if (!week) return null;
|
|
4101
|
-
const
|
|
4102
|
-
|
|
4103
|
-
week.yOffset + (totalMinutes / 1440) * week.height - this.scrollTop
|
|
4304
|
+
const dayIndex = week.days.findIndex(
|
|
4305
|
+
(d) => d.toDateString() === day.toDateString(),
|
|
4104
4306
|
);
|
|
4307
|
+
if (dayIndex < 0) return null;
|
|
4308
|
+
const { row } = this.getDayVisualPosition(dayIndex);
|
|
4309
|
+
const totalMinutes = hours * 60 + minutes;
|
|
4310
|
+
const rowY = week.yOffset + row * this.dayHeight;
|
|
4311
|
+
return rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop;
|
|
4105
4312
|
};
|
|
4106
4313
|
|
|
4107
4314
|
const getDayColumnX = (day: Date) => {
|
|
@@ -4113,7 +4320,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
4113
4320
|
(d) => d.toDateString() === day.toDateString(),
|
|
4114
4321
|
);
|
|
4115
4322
|
if (dayIndex < 0) return null;
|
|
4116
|
-
|
|
4323
|
+
const { col } = this.getDayVisualPosition(dayIndex);
|
|
4324
|
+
return col * dayWidth;
|
|
4117
4325
|
};
|
|
4118
4326
|
|
|
4119
4327
|
const drawBlock = (colX: number, top: number, bottom: number) => {
|
|
@@ -4255,7 +4463,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4255
4463
|
|
|
4256
4464
|
ctx.clearRect(0, 0, width, height);
|
|
4257
4465
|
|
|
4258
|
-
const dayWidth = width /
|
|
4466
|
+
const dayWidth = width / this._columnsPerRow;
|
|
4259
4467
|
const scrollTop = this.scrollTop;
|
|
4260
4468
|
const fontFamily = getComputedStyle(this).fontFamily;
|
|
4261
4469
|
|
|
@@ -4275,16 +4483,18 @@ export class CalendarViewElement extends LitElement {
|
|
|
4275
4483
|
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
4276
4484
|
const day = week.days[dayIndex];
|
|
4277
4485
|
if (!day) continue;
|
|
4278
|
-
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
4486
|
+
|
|
4487
|
+
const { row, col } = this.getDayVisualPosition(dayIndex);
|
|
4488
|
+
const x = col * dayWidth;
|
|
4489
|
+
const dayTop = week.yOffset + row * this.dayHeight - scrollTop;
|
|
4490
|
+
const dayBottom = dayTop + this.dayHeight;
|
|
4281
4491
|
|
|
4282
4492
|
if (dayIndex === 5 || dayIndex === 6) {
|
|
4283
4493
|
const bgWeekend =
|
|
4284
4494
|
getComputedStyle(this).getPropertyValue("--bg-weekend").trim() ||
|
|
4285
4495
|
"rgba(255, 255, 255, 0.03)";
|
|
4286
4496
|
ctx.fillStyle = bgWeekend;
|
|
4287
|
-
ctx.fillRect(x, dayTop, dayWidth,
|
|
4497
|
+
ctx.fillRect(x, dayTop, dayWidth, this.dayHeight);
|
|
4288
4498
|
}
|
|
4289
4499
|
|
|
4290
4500
|
// Draw red outline for current day in zoomed-out view
|
|
@@ -4297,14 +4507,14 @@ export class CalendarViewElement extends LitElement {
|
|
|
4297
4507
|
.trim() || "rgba(255, 0, 0, 0.8)";
|
|
4298
4508
|
ctx.strokeStyle = accentTime;
|
|
4299
4509
|
ctx.lineWidth = 1;
|
|
4300
|
-
ctx.strokeRect(x + 1, dayTop + 1, dayWidth - 2,
|
|
4510
|
+
ctx.strokeRect(x + 1, dayTop + 1, dayWidth - 2, this.dayHeight - 2);
|
|
4301
4511
|
ctx.lineWidth = 1;
|
|
4302
4512
|
}
|
|
4303
4513
|
|
|
4304
4514
|
const labelHeight = 12;
|
|
4305
4515
|
const labelBottomMargin = 64;
|
|
4306
4516
|
const labelY = Math.min(
|
|
4307
|
-
dayBottom - labelHeight,
|
|
4517
|
+
dayBottom - labelHeight - 8,
|
|
4308
4518
|
height - labelHeight - labelBottomMargin,
|
|
4309
4519
|
);
|
|
4310
4520
|
|
|
@@ -4316,6 +4526,78 @@ export class CalendarViewElement extends LitElement {
|
|
|
4316
4526
|
}
|
|
4317
4527
|
}
|
|
4318
4528
|
|
|
4529
|
+
renderWeekdayLabels(
|
|
4530
|
+
ctx: CanvasRenderingContext2D,
|
|
4531
|
+
dayWidth: number,
|
|
4532
|
+
visibleWeeks: WeekInfo[],
|
|
4533
|
+
scrollTop: number,
|
|
4534
|
+
height: number,
|
|
4535
|
+
): void {
|
|
4536
|
+
if (visibleWeeks.length === 0) return;
|
|
4537
|
+
|
|
4538
|
+
const weekdayNames = this.internal.getWeekdayNames();
|
|
4539
|
+
const fontFamily = getComputedStyle(this).fontFamily;
|
|
4540
|
+
const textMuted =
|
|
4541
|
+
getComputedStyle(this).getPropertyValue("--text-muted").trim() ||
|
|
4542
|
+
"rgba(255, 255, 255, 0.4)";
|
|
4543
|
+
const bgPrimary =
|
|
4544
|
+
getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
|
|
4545
|
+
"rgba(30, 30, 30, 0.9)";
|
|
4546
|
+
|
|
4547
|
+
ctx.font = `500 12px ${fontFamily}`;
|
|
4548
|
+
ctx.textAlign = "center";
|
|
4549
|
+
ctx.textBaseline = "top";
|
|
4550
|
+
|
|
4551
|
+
const labelHeight = 16;
|
|
4552
|
+
const labelY = 12; // Below month label
|
|
4553
|
+
|
|
4554
|
+
// Find the first visible visual row
|
|
4555
|
+
const firstWeek = visibleWeeks[0];
|
|
4556
|
+
if (!firstWeek) return;
|
|
4557
|
+
|
|
4558
|
+
// Determine which visual rows are visible
|
|
4559
|
+
for (let row = 0; row < this.rowsPerWeek; row++) {
|
|
4560
|
+
const rowTop = firstWeek.yOffset + row * this.dayHeight - scrollTop;
|
|
4561
|
+
const rowBottom = rowTop + this.dayHeight;
|
|
4562
|
+
|
|
4563
|
+
// Check if this visual row is visible
|
|
4564
|
+
if (rowBottom < 0 || rowTop > height) continue;
|
|
4565
|
+
|
|
4566
|
+
// Calculate sticky Y position - stays at top but doesn't go past row bottom
|
|
4567
|
+
const stickyY = Math.min(labelY, rowBottom - labelHeight - 2);
|
|
4568
|
+
if (stickyY < 0) continue;
|
|
4569
|
+
|
|
4570
|
+
// Draw weekday labels for each column in this visual row
|
|
4571
|
+
for (let col = 0; col < this._columnsPerRow; col++) {
|
|
4572
|
+
const dayIndex = row * this._columnsPerRow + col;
|
|
4573
|
+
if (dayIndex >= 7) continue;
|
|
4574
|
+
const dayName = weekdayNames[dayIndex];
|
|
4575
|
+
if (!dayName) continue;
|
|
4576
|
+
|
|
4577
|
+
const x = LEFT_GUTTER_WIDTH + col * dayWidth + dayWidth / 2;
|
|
4578
|
+
|
|
4579
|
+
// Draw background pill
|
|
4580
|
+
const textWidth = ctx.measureText(dayName).width;
|
|
4581
|
+
const bgPaddingX = 6;
|
|
4582
|
+
const bgPaddingY = 2;
|
|
4583
|
+
ctx.fillStyle = bgPrimary;
|
|
4584
|
+
ctx.beginPath();
|
|
4585
|
+
ctx.roundRect(
|
|
4586
|
+
x - textWidth / 2 - bgPaddingX,
|
|
4587
|
+
stickyY,
|
|
4588
|
+
textWidth + bgPaddingX * 2,
|
|
4589
|
+
labelHeight,
|
|
4590
|
+
4,
|
|
4591
|
+
);
|
|
4592
|
+
ctx.fill();
|
|
4593
|
+
|
|
4594
|
+
// Draw text
|
|
4595
|
+
ctx.fillStyle = textMuted;
|
|
4596
|
+
ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4319
4601
|
renderSelection(): ReturnType<typeof html> {
|
|
4320
4602
|
if (!this.selection) return html``;
|
|
4321
4603
|
|
|
@@ -4512,14 +4794,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
4512
4794
|
|
|
4513
4795
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
4514
4796
|
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
4515
|
-
const dayWidth = gridWidth /
|
|
4797
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4516
4798
|
|
|
4517
4799
|
// Check if X is in the calendar grid area
|
|
4518
4800
|
if (x < LEFT_GUTTER_WIDTH) return null;
|
|
4519
4801
|
|
|
4520
|
-
const
|
|
4802
|
+
const col = Math.max(
|
|
4521
4803
|
0,
|
|
4522
|
-
Math.min(
|
|
4804
|
+
Math.min(
|
|
4805
|
+
this._columnsPerRow - 1,
|
|
4806
|
+
Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
4807
|
+
),
|
|
4523
4808
|
);
|
|
4524
4809
|
|
|
4525
4810
|
// Find week at Y position
|
|
@@ -4529,6 +4814,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
4529
4814
|
|
|
4530
4815
|
if (!week) return null;
|
|
4531
4816
|
|
|
4817
|
+
// Calculate which visual row within the week
|
|
4818
|
+
const rowInWeek = Math.floor((y - week.yOffset) / this.dayHeight);
|
|
4819
|
+
const dayIndex = rowInWeek * this._columnsPerRow + col;
|
|
4820
|
+
|
|
4821
|
+
if (dayIndex < 0 || dayIndex > 6) return null;
|
|
4822
|
+
|
|
4532
4823
|
const day = week.days[dayIndex];
|
|
4533
4824
|
if (!day) return null;
|
|
4534
4825
|
|
|
@@ -4537,9 +4828,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
4537
4828
|
let minute = 0;
|
|
4538
4829
|
|
|
4539
4830
|
if (this.dayHeight >= 200) {
|
|
4540
|
-
const
|
|
4541
|
-
|
|
4542
|
-
const minutes = Math.floor((offsetInWeek / week.height) * 24 * 60);
|
|
4831
|
+
const offsetInRow = y - (week.yOffset + rowInWeek * this.dayHeight);
|
|
4832
|
+
const minutes = Math.floor((offsetInRow / this.dayHeight) * 24 * 60);
|
|
4543
4833
|
hour = Math.floor(minutes / 60);
|
|
4544
4834
|
minute = minutes % 60;
|
|
4545
4835
|
} else {
|
|
@@ -4558,7 +4848,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4558
4848
|
|
|
4559
4849
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
4560
4850
|
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
4561
|
-
const dayWidth = gridWidth /
|
|
4851
|
+
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4562
4852
|
|
|
4563
4853
|
// Find the week that contains this date
|
|
4564
4854
|
const dateStr = date.toDateString();
|
|
@@ -4572,15 +4862,18 @@ export class CalendarViewElement extends LitElement {
|
|
|
4572
4862
|
const dayIndex = week.days.findIndex((d) => d.toDateString() === dateStr);
|
|
4573
4863
|
if (dayIndex === -1) return null;
|
|
4574
4864
|
|
|
4865
|
+
// Get visual position
|
|
4866
|
+
const { row, col } = this.getDayVisualPosition(dayIndex);
|
|
4867
|
+
|
|
4575
4868
|
// Calculate X position
|
|
4576
|
-
const x = LEFT_GUTTER_WIDTH +
|
|
4869
|
+
const x = LEFT_GUTTER_WIDTH + col * dayWidth + dayWidth / 2;
|
|
4577
4870
|
|
|
4578
4871
|
// Calculate Y position based on time
|
|
4579
4872
|
const hours = date.getHours();
|
|
4580
4873
|
const minutes = date.getMinutes();
|
|
4581
4874
|
const totalMinutes = hours * 60 + minutes;
|
|
4582
|
-
const
|
|
4583
|
-
const y = week.yOffset +
|
|
4875
|
+
const offsetInRow = (totalMinutes / (24 * 60)) * this.dayHeight;
|
|
4876
|
+
const y = week.yOffset + row * this.dayHeight + offsetInRow;
|
|
4584
4877
|
|
|
4585
4878
|
return { x, y };
|
|
4586
4879
|
}
|
|
@@ -4669,7 +4962,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
4669
4962
|
return html`<div class="notification-empty-state">No notifications set</div>`;
|
|
4670
4963
|
}
|
|
4671
4964
|
|
|
4672
|
-
return event.reminders.map(
|
|
4965
|
+
return event.reminders.map(
|
|
4966
|
+
(notif) => html`
|
|
4673
4967
|
<div class="notification-item">
|
|
4674
4968
|
<select
|
|
4675
4969
|
class="notification-select"
|
|
@@ -4679,14 +4973,20 @@ export class CalendarViewElement extends LitElement {
|
|
|
4679
4973
|
this.updateNotification(event, notif.id, { triggerOffset: offset });
|
|
4680
4974
|
}}
|
|
4681
4975
|
>
|
|
4682
|
-
${NOTIFICATION_PRESETS.map(
|
|
4976
|
+
${NOTIFICATION_PRESETS.map(
|
|
4977
|
+
(p) =>
|
|
4978
|
+
html`<option value="${p.value}" ?selected=${
|
|
4979
|
+
p.value === notif.triggerOffset
|
|
4980
|
+
}>${p.label}</option>`,
|
|
4981
|
+
)}
|
|
4683
4982
|
</select>
|
|
4684
4983
|
<button
|
|
4685
4984
|
class="notification-remove-button"
|
|
4686
4985
|
@click=${() => this.removeNotification(event, notif.id)}
|
|
4687
4986
|
>×</button>
|
|
4688
4987
|
</div>
|
|
4689
|
-
|
|
4988
|
+
`,
|
|
4989
|
+
);
|
|
4690
4990
|
}
|
|
4691
4991
|
|
|
4692
4992
|
addNotification(event: CalendarEvent) {
|
|
@@ -4696,31 +4996,41 @@ export class CalendarViewElement extends LitElement {
|
|
|
4696
4996
|
enabled: true,
|
|
4697
4997
|
};
|
|
4698
4998
|
const reminders = [...(event.reminders || []), newNotif];
|
|
4699
|
-
this.dispatchEvent(
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4999
|
+
this.dispatchEvent(
|
|
5000
|
+
new CustomEvent("update-event", {
|
|
5001
|
+
detail: { event, updates: { reminders } },
|
|
5002
|
+
bubbles: true,
|
|
5003
|
+
composed: true,
|
|
5004
|
+
}),
|
|
5005
|
+
);
|
|
4704
5006
|
}
|
|
4705
5007
|
|
|
4706
|
-
updateNotification(
|
|
4707
|
-
|
|
4708
|
-
|
|
5008
|
+
updateNotification(
|
|
5009
|
+
event: CalendarEvent,
|
|
5010
|
+
notifId: string,
|
|
5011
|
+
updates: { triggerOffset: number },
|
|
5012
|
+
) {
|
|
5013
|
+
const reminders = (event.reminders || []).map((n) =>
|
|
5014
|
+
n.id === notifId ? { ...n, ...updates } : n,
|
|
5015
|
+
);
|
|
5016
|
+
this.dispatchEvent(
|
|
5017
|
+
new CustomEvent("update-event", {
|
|
5018
|
+
detail: { event, updates: { reminders } },
|
|
5019
|
+
bubbles: true,
|
|
5020
|
+
composed: true,
|
|
5021
|
+
}),
|
|
4709
5022
|
);
|
|
4710
|
-
this.dispatchEvent(new CustomEvent("update-event", {
|
|
4711
|
-
detail: { event, updates: { reminders } },
|
|
4712
|
-
bubbles: true,
|
|
4713
|
-
composed: true,
|
|
4714
|
-
}));
|
|
4715
5023
|
}
|
|
4716
5024
|
|
|
4717
5025
|
removeNotification(event: CalendarEvent, notifId: string) {
|
|
4718
|
-
const reminders = (event.reminders || []).filter(n => n.id !== notifId);
|
|
4719
|
-
this.dispatchEvent(
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
5026
|
+
const reminders = (event.reminders || []).filter((n) => n.id !== notifId);
|
|
5027
|
+
this.dispatchEvent(
|
|
5028
|
+
new CustomEvent("update-event", {
|
|
5029
|
+
detail: { event, updates: { reminders } },
|
|
5030
|
+
bubbles: true,
|
|
5031
|
+
composed: true,
|
|
5032
|
+
}),
|
|
5033
|
+
);
|
|
4724
5034
|
}
|
|
4725
5035
|
|
|
4726
5036
|
shouldRenderEventWithStripes(event: CalendarEvent): boolean {
|
|
@@ -4809,9 +5119,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
4809
5119
|
const containerHeight = this.scrollContainer?.clientHeight || 600;
|
|
4810
5120
|
|
|
4811
5121
|
// Try positioning to the right, fallback to left if no space
|
|
4812
|
-
const rightPosition =
|
|
5122
|
+
const rightPosition =
|
|
5123
|
+
this.selectedEventRect.x + this.selectedEventRect.width + GAP;
|
|
4813
5124
|
const leftPosition = this.selectedEventRect.x - OVERLAY_WIDTH - GAP;
|
|
4814
|
-
const preferredLeft =
|
|
5125
|
+
const preferredLeft =
|
|
5126
|
+
rightPosition + OVERLAY_WIDTH <= containerWidth
|
|
5127
|
+
? rightPosition
|
|
5128
|
+
: leftPosition;
|
|
4815
5129
|
|
|
4816
5130
|
// Clamp to viewport boundaries
|
|
4817
5131
|
const minLeft = LEFT_GUTTER_WIDTH + GAP;
|
|
@@ -4829,24 +5143,22 @@ export class CalendarViewElement extends LitElement {
|
|
|
4829
5143
|
|
|
4830
5144
|
return html`
|
|
4831
5145
|
<div class="event-detail-overlay" style="${style}">
|
|
4832
|
-
<div class="event-detail-color-bar" style="opacity: ${
|
|
4833
|
-
event.readOnly ? "0.5" : "1"
|
|
4834
|
-
}"></div>
|
|
4835
|
-
|
|
4836
5146
|
<div class="event-detail-content">
|
|
4837
5147
|
<div class="event-detail-header">
|
|
4838
5148
|
<div class="event-detail-header-content">
|
|
4839
5149
|
${
|
|
4840
|
-
|
|
4841
|
-
|
|
5150
|
+
event.calendar
|
|
5151
|
+
? html`
|
|
4842
5152
|
<div class="event-detail-section event-detail-calendar">
|
|
4843
5153
|
<div class="event-detail-value">${event.calendar}</div>
|
|
4844
5154
|
</div>
|
|
4845
5155
|
`
|
|
4846
|
-
|
|
4847
|
-
|
|
5156
|
+
: null
|
|
5157
|
+
}
|
|
4848
5158
|
${
|
|
4849
|
-
event.readOnly
|
|
5159
|
+
event.readOnly ||
|
|
5160
|
+
(event.organizer != null &&
|
|
5161
|
+
!this.currentUserEmails.has(event.organizer.email))
|
|
4850
5162
|
? html`
|
|
4851
5163
|
<h3 class="event-detail-title">${
|
|
4852
5164
|
event.rrule ? html`<span style="opacity: 0.6">⟳</span> ` : ""
|
|
@@ -5004,21 +5316,32 @@ export class CalendarViewElement extends LitElement {
|
|
|
5004
5316
|
: null;
|
|
5005
5317
|
})()}
|
|
5006
5318
|
|
|
5007
|
-
${
|
|
5319
|
+
${
|
|
5320
|
+
!event.readOnly
|
|
5321
|
+
? html`
|
|
5008
5322
|
<div class="event-detail-section">
|
|
5009
|
-
<div class="event-detail-label">
|
|
5323
|
+
<div class="event-detail-label">
|
|
5324
|
+
<span>Notifications</span>
|
|
5325
|
+
|
|
5326
|
+
<button class="notification-add-button" title="Add notification" @click=${() =>
|
|
5327
|
+
this.addNotification(event)}>
|
|
5328
|
+
+
|
|
5329
|
+
</button>
|
|
5330
|
+
</div>
|
|
5010
5331
|
<div class="event-notifications">
|
|
5011
5332
|
${this.renderNotificationsList(event)}
|
|
5012
|
-
<button class="notification-add-button" @click=${() => this.addNotification(event)}>
|
|
5013
|
-
+ Add notification
|
|
5014
|
-
</button>
|
|
5015
5333
|
</div>
|
|
5016
5334
|
</div>
|
|
5017
|
-
`
|
|
5335
|
+
`
|
|
5336
|
+
: null
|
|
5337
|
+
}
|
|
5018
5338
|
|
|
5019
5339
|
${
|
|
5020
|
-
event.
|
|
5021
|
-
|
|
5340
|
+
event.readOnly ||
|
|
5341
|
+
(event.organizer != null &&
|
|
5342
|
+
!this.currentUserEmails.has(event.organizer.email))
|
|
5343
|
+
? event.description
|
|
5344
|
+
? html`
|
|
5022
5345
|
<div class="event-detail-section">
|
|
5023
5346
|
<div class="event-detail-label">Description</div>
|
|
5024
5347
|
<div class="event-detail-description ${
|
|
@@ -5048,7 +5371,58 @@ export class CalendarViewElement extends LitElement {
|
|
|
5048
5371
|
}
|
|
5049
5372
|
</div>
|
|
5050
5373
|
`
|
|
5051
|
-
|
|
5374
|
+
: null
|
|
5375
|
+
: html`
|
|
5376
|
+
<div class="event-detail-section">
|
|
5377
|
+
<div class="event-detail-label">Description</div>
|
|
5378
|
+
<textarea
|
|
5379
|
+
class="event-detail-description-input"
|
|
5380
|
+
.value=${event.description ?? ""}
|
|
5381
|
+
placeholder="Add description..."
|
|
5382
|
+
rows="3"
|
|
5383
|
+
@input=${(e: Event) => {
|
|
5384
|
+
const input = e.target as HTMLTextAreaElement;
|
|
5385
|
+
const newDescription = input.value;
|
|
5386
|
+
if (this.updateEventTimeout) {
|
|
5387
|
+
clearTimeout(this.updateEventTimeout);
|
|
5388
|
+
}
|
|
5389
|
+
this.updateEventTimeout = setTimeout(() => {
|
|
5390
|
+
this.dispatchEvent(
|
|
5391
|
+
new CustomEvent("update-event", {
|
|
5392
|
+
detail: {
|
|
5393
|
+
event,
|
|
5394
|
+
updates: { description: newDescription },
|
|
5395
|
+
},
|
|
5396
|
+
bubbles: true,
|
|
5397
|
+
composed: true,
|
|
5398
|
+
}),
|
|
5399
|
+
);
|
|
5400
|
+
this.updateEventTimeout = null;
|
|
5401
|
+
}, 500);
|
|
5402
|
+
}}
|
|
5403
|
+
@blur=${(e: Event) => {
|
|
5404
|
+
const input = e.target as HTMLTextAreaElement;
|
|
5405
|
+
const newDescription = input.value;
|
|
5406
|
+
if (this.updateEventTimeout) {
|
|
5407
|
+
clearTimeout(this.updateEventTimeout);
|
|
5408
|
+
this.updateEventTimeout = null;
|
|
5409
|
+
}
|
|
5410
|
+
if (newDescription !== (event.description ?? "")) {
|
|
5411
|
+
this.dispatchEvent(
|
|
5412
|
+
new CustomEvent("update-event", {
|
|
5413
|
+
detail: {
|
|
5414
|
+
event,
|
|
5415
|
+
updates: { description: newDescription },
|
|
5416
|
+
},
|
|
5417
|
+
bubbles: true,
|
|
5418
|
+
composed: true,
|
|
5419
|
+
}),
|
|
5420
|
+
);
|
|
5421
|
+
}
|
|
5422
|
+
}}
|
|
5423
|
+
/>
|
|
5424
|
+
</div>
|
|
5425
|
+
`
|
|
5052
5426
|
}
|
|
5053
5427
|
</div>
|
|
5054
5428
|
|
|
@@ -5113,15 +5487,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
5113
5487
|
<div class="container ${this.isDraggingFile ? "dragging-file" : ""}">
|
|
5114
5488
|
<div class="toolbar">
|
|
5115
5489
|
<div class="toolbar-left">
|
|
5116
|
-
<
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
</button>
|
|
5121
|
-
<button class="toolbar-button" title="Upcoming Notifications" @click=${
|
|
5122
|
-
this.onNotificationPopoverToggle
|
|
5490
|
+
<slot name="toolbar-center"></slot>
|
|
5491
|
+
|
|
5492
|
+
<button class="toolbar-button" title="Month" @click=${
|
|
5493
|
+
this.scrollToMonth
|
|
5123
5494
|
}>
|
|
5124
|
-
|
|
5495
|
+
Month
|
|
5125
5496
|
</button>
|
|
5126
5497
|
<button class="toolbar-button" title="Today" @click=${
|
|
5127
5498
|
this.scrollToToday
|
|
@@ -5140,7 +5511,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
5140
5511
|
→
|
|
5141
5512
|
</button>
|
|
5142
5513
|
<div class="toolbar-zoom">
|
|
5143
|
-
<span class="toolbar-zoom-label">Zoom</span>
|
|
5144
5514
|
<input
|
|
5145
5515
|
type="range"
|
|
5146
5516
|
class="toolbar-zoom-slider"
|
|
@@ -5175,19 +5545,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
5175
5545
|
@input=${this.onFilterInput}
|
|
5176
5546
|
/>
|
|
5177
5547
|
</div>
|
|
5178
|
-
|
|
5179
|
-
<slot name="toolbar-center"></slot>
|
|
5180
5548
|
</div>
|
|
5181
5549
|
</div>
|
|
5182
|
-
|
|
5183
|
-
<div class="header">
|
|
5184
|
-
<div class="header-gutter"></div>
|
|
5185
|
-
<div class="weekdays">
|
|
5186
|
-
${this.internal
|
|
5187
|
-
.getWeekdayNames()
|
|
5188
|
-
.map((name) => html`<div class="weekday">${name}</div>`)}
|
|
5189
|
-
</div>
|
|
5190
|
-
</div>
|
|
5191
5550
|
|
|
5192
5551
|
<div class="body">
|
|
5193
5552
|
<div class="calendar-area">
|
|
@@ -5243,31 +5602,45 @@ export class CalendarViewElement extends LitElement {
|
|
|
5243
5602
|
};
|
|
5244
5603
|
|
|
5245
5604
|
return html`
|
|
5246
|
-
<div class="notification-popover-overlay" @click=${
|
|
5605
|
+
<div class="notification-popover-overlay" @click=${
|
|
5606
|
+
this.onNotificationPopoverToggle
|
|
5607
|
+
}></div>
|
|
5247
5608
|
<div class="notification-popover">
|
|
5248
5609
|
<div class="notification-popover-header">
|
|
5249
5610
|
<h3>Upcoming Notifications</h3>
|
|
5250
|
-
<button class="notification-popover-close" @click=${
|
|
5611
|
+
<button class="notification-popover-close" @click=${
|
|
5612
|
+
this.onNotificationPopoverToggle
|
|
5613
|
+
}>×</button>
|
|
5251
5614
|
</div>
|
|
5252
5615
|
<div class="notification-popover-content">
|
|
5253
|
-
${
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5616
|
+
${
|
|
5617
|
+
this.scheduledNotifications.length === 0
|
|
5618
|
+
? html`<div class="notification-popover-empty">No scheduled notifications</div>`
|
|
5619
|
+
: this.scheduledNotifications.map(
|
|
5620
|
+
(notif) => html`
|
|
5257
5621
|
<div class="notification-popover-item">
|
|
5258
5622
|
<div class="notification-popover-item-header">
|
|
5259
|
-
<div class="notification-popover-item-title">${
|
|
5260
|
-
|
|
5623
|
+
<div class="notification-popover-item-title">${
|
|
5624
|
+
notif.eventTitle
|
|
5625
|
+
}</div>
|
|
5626
|
+
<div class="notification-popover-item-time">${formatTriggerTime(
|
|
5627
|
+
notif.triggerTime,
|
|
5628
|
+
)}</div>
|
|
5261
5629
|
</div>
|
|
5262
5630
|
<div class="notification-popover-item-details">
|
|
5263
|
-
<div class="notification-popover-item-event-time">📅 ${formatEventTime(
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5631
|
+
<div class="notification-popover-item-event-time">📅 ${formatEventTime(
|
|
5632
|
+
notif.eventStart,
|
|
5633
|
+
)}</div>
|
|
5634
|
+
${
|
|
5635
|
+
notif.eventLocation
|
|
5636
|
+
? html`<div class="notification-popover-item-location">📍 ${notif.eventLocation}</div>`
|
|
5637
|
+
: null
|
|
5638
|
+
}
|
|
5267
5639
|
</div>
|
|
5268
5640
|
</div>
|
|
5269
5641
|
`,
|
|
5270
|
-
|
|
5642
|
+
)
|
|
5643
|
+
}
|
|
5271
5644
|
</div>
|
|
5272
5645
|
</div>
|
|
5273
5646
|
`;
|