@luckydye/calendar 1.3.1 → 1.4.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/dist/calendar.js +2777 -2010
- package/package.json +7 -1
- package/src/ActiveCalendarStore.ts +88 -88
- package/src/CalDAVConfig.ts +611 -514
- package/src/CalDAVSource.ts +561 -433
- package/src/CalendarIntegration.ts +64 -47
- package/src/CalendarInternal.ts +645 -613
- package/src/CalendarLayer.ts +1 -0
- package/src/CalendarStorage.ts +51 -48
- package/src/CalendarView.ts +1085 -505
- package/src/Color.ts +48 -54
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +758 -661
- package/src/ICal.ts +420 -343
- package/src/InMemorySource.ts +56 -48
- package/src/IndexedDBStorage.ts +444 -395
- package/src/InhouseBookingSource.ts +614 -522
- package/src/Keybinds.ts +6 -1
- package/src/NotificationScheduler.ts +11 -8
- package/src/StatusBar.ts +12 -8
- package/src/StatusMessage.ts +2 -2
- package/src/Theme.ts +21 -7
- package/src/TimeseriesJson.ts +98 -98
- package/src/app.ts +301 -115
- package/src/layers/EventsLayer.ts +530 -400
- package/src/layers/GridLayer.ts +45 -125
- package/src/layers/TimeseriesHeatmapLayer.ts +123 -120
- package/src/service-worker.js +3 -2
package/src/CalendarView.ts
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
|
-
import { css, html,
|
|
1
|
+
import { LitElement, css, html, render } from "lit";
|
|
2
2
|
import {
|
|
3
3
|
type Attendee,
|
|
4
4
|
type CalendarEvent,
|
|
5
5
|
CalendarInternal,
|
|
6
|
-
type WeekInfo,
|
|
7
6
|
NOTIFICATION_PRESETS,
|
|
7
|
+
type WeekInfo,
|
|
8
8
|
} from "./CalendarInternal.js";
|
|
9
|
+
import {
|
|
10
|
+
type CalendarLayer,
|
|
11
|
+
type LayerContext,
|
|
12
|
+
TIME_SCALE_DAY_HEIGHT,
|
|
13
|
+
} from "./CalendarLayer.js";
|
|
9
14
|
import { hexToRgb, rgbToHsl } from "./Color.js";
|
|
15
|
+
import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
|
|
16
|
+
import { serializeEventsToICal } from "./ICal.js";
|
|
17
|
+
import { IndexedDBStorage } from "./IndexedDBStorage.js";
|
|
18
|
+
import type { StatusBarData } from "./StatusBar.js";
|
|
19
|
+
import { queueStatus } from "./StatusMessage.js";
|
|
10
20
|
import {
|
|
11
21
|
type ThemeName,
|
|
12
22
|
applyTheme,
|
|
13
|
-
saveThemePreference,
|
|
14
|
-
loadThemePreference,
|
|
15
23
|
availableThemes,
|
|
24
|
+
loadThemePreference,
|
|
25
|
+
saveThemePreference,
|
|
16
26
|
} from "./Theme.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
import {
|
|
28
|
+
type EventRect,
|
|
29
|
+
type EventsState,
|
|
30
|
+
type PreviewEventData,
|
|
31
|
+
createEventsLayer,
|
|
32
|
+
} from "./layers/EventsLayer.js";
|
|
22
33
|
import { createGridLayer } from "./layers/GridLayer.js";
|
|
23
|
-
import { createEventsLayer, type EventRect, type EventsState } from "./layers/EventsLayer.js";
|
|
24
34
|
import { createTimeseriesHeatmapLayer } from "./layers/TimeseriesHeatmapLayer.js";
|
|
25
35
|
|
|
26
36
|
const MIN_DAY_HEIGHT = 50;
|
|
27
37
|
const MAX_DAY_HEIGHT = 3000; // 1px per minute
|
|
28
38
|
const LEFT_GUTTER_WIDTH = 60;
|
|
29
39
|
const MINIMAP_WIDTH = 12;
|
|
40
|
+
const MIN_DAY_COLUMN_WIDTH = 80;
|
|
41
|
+
const MAX_DAY_COLUMN_WIDTH = 320;
|
|
42
|
+
const DEFAULT_DAY_COLUMN_WIDTH = 120;
|
|
43
|
+
const TIME_SCALE_EVENT_THRESHOLD = 400;
|
|
44
|
+
const FILTER_CONTEXT_WEEKS = 8;
|
|
30
45
|
|
|
31
46
|
export class CalendarViewElement extends LitElement {
|
|
32
47
|
static styles = css`
|
|
@@ -64,7 +79,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
64
79
|
bottom: 0;
|
|
65
80
|
left: 0;
|
|
66
81
|
right: 0;
|
|
67
|
-
z-index:
|
|
82
|
+
z-index: 200;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
.toolbar::before {
|
|
@@ -159,6 +174,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
159
174
|
gap: 8px;
|
|
160
175
|
}
|
|
161
176
|
|
|
177
|
+
.toolbar-slider-label {
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.6));
|
|
180
|
+
user-select: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
162
183
|
.toolbar-zoom-slider {
|
|
163
184
|
width: 100px;
|
|
164
185
|
height: 4px;
|
|
@@ -230,10 +251,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
230
251
|
position: absolute;
|
|
231
252
|
inset: 0;
|
|
232
253
|
overflow-y: overlay;
|
|
233
|
-
overflow-x:
|
|
254
|
+
overflow-x: auto;
|
|
234
255
|
z-index: 1;
|
|
235
256
|
cursor: default;
|
|
236
257
|
overflow-anchor: none;
|
|
258
|
+
touch-action: pan-x pan-y;
|
|
237
259
|
}
|
|
238
260
|
|
|
239
261
|
:host([scroll-lock]) .scroll-container {
|
|
@@ -259,7 +281,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
259
281
|
left: 60px;
|
|
260
282
|
right: 12px;
|
|
261
283
|
pointer-events: none;
|
|
262
|
-
z-index:
|
|
284
|
+
z-index: 101;
|
|
263
285
|
overflow: hidden;
|
|
264
286
|
}
|
|
265
287
|
|
|
@@ -300,7 +322,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
300
322
|
flex-direction: row;
|
|
301
323
|
backdrop-filter: blur(10px);
|
|
302
324
|
box-shadow: var(--shadow-overlay, 0 4px 12px rgba(0, 0, 0, 0.9));
|
|
303
|
-
z-index:
|
|
325
|
+
z-index: 102;
|
|
304
326
|
}
|
|
305
327
|
|
|
306
328
|
.event-detail-header {
|
|
@@ -679,6 +701,19 @@ export class CalendarViewElement extends LitElement {
|
|
|
679
701
|
text-align: left;
|
|
680
702
|
}
|
|
681
703
|
|
|
704
|
+
.description-actions {
|
|
705
|
+
display: flex;
|
|
706
|
+
gap: 12px;
|
|
707
|
+
align-items: center;
|
|
708
|
+
flex-wrap: wrap;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.description-actions .description-see-more {
|
|
712
|
+
display: inline-block;
|
|
713
|
+
margin-top: 8px;
|
|
714
|
+
padding: 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
682
717
|
.description-see-more:hover {
|
|
683
718
|
color: var(--accent-hover, rgb(120, 170, 255));
|
|
684
719
|
}
|
|
@@ -728,6 +763,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
728
763
|
return this._dayHeight;
|
|
729
764
|
}
|
|
730
765
|
|
|
766
|
+
_minDayColumnWidth = DEFAULT_DAY_COLUMN_WIDTH;
|
|
767
|
+
set minDayColumnWidth(value) {
|
|
768
|
+
this._minDayColumnWidth = value;
|
|
769
|
+
this.saveMinDayColumnWidth();
|
|
770
|
+
this.renderCanvas();
|
|
771
|
+
this.requestUpdate();
|
|
772
|
+
}
|
|
773
|
+
get minDayColumnWidth() {
|
|
774
|
+
return this._minDayColumnWidth;
|
|
775
|
+
}
|
|
776
|
+
|
|
731
777
|
_scrollTop = 0;
|
|
732
778
|
set scrollTop(value) {
|
|
733
779
|
this._scrollTop = value;
|
|
@@ -735,7 +781,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
735
781
|
if (!this.scrollContainer || !this.scrollContent) return;
|
|
736
782
|
|
|
737
783
|
if (this.scrollContainer.scrollHeight < value) {
|
|
738
|
-
this.scrollContent.style.minHeight = value + window.innerHeight
|
|
784
|
+
this.scrollContent.style.minHeight = `${value + window.innerHeight}px`;
|
|
739
785
|
}
|
|
740
786
|
|
|
741
787
|
this.scrollContainer.scrollTop = value;
|
|
@@ -783,10 +829,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
783
829
|
|
|
784
830
|
const progress = Math.min((currentTime - startTime) / duration, 1);
|
|
785
831
|
|
|
786
|
-
this._dayHeight =
|
|
832
|
+
this._dayHeight =
|
|
833
|
+
startDayHeight +
|
|
834
|
+
(toDayHeight - startDayHeight) * easeOutExpo(progress);
|
|
787
835
|
this.updateWeekOffsets();
|
|
788
836
|
|
|
789
|
-
const currentFrac =
|
|
837
|
+
const currentFrac =
|
|
838
|
+
startFrac + (targetFrac - startFrac) * easeOutCubic(progress);
|
|
790
839
|
this._scrollTop = Math.max(0, currentFrac * this.totalHeight);
|
|
791
840
|
|
|
792
841
|
if (this.scrollContainer) {
|
|
@@ -794,8 +843,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
794
843
|
this.scrollContent &&
|
|
795
844
|
this.scrollContainer.scrollHeight < this._scrollTop
|
|
796
845
|
) {
|
|
797
|
-
this.scrollContent.style.minHeight =
|
|
798
|
-
this._scrollTop + window.innerHeight
|
|
846
|
+
this.scrollContent.style.minHeight = `${
|
|
847
|
+
this._scrollTop + window.innerHeight
|
|
848
|
+
}px`;
|
|
799
849
|
}
|
|
800
850
|
this.scrollContainer.scrollTop = this._scrollTop;
|
|
801
851
|
}
|
|
@@ -838,6 +888,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
838
888
|
this.renderCanvas();
|
|
839
889
|
}
|
|
840
890
|
|
|
891
|
+
syncScrollDomToState(): void {
|
|
892
|
+
if (!this.scrollContainer || !this.scrollContent) return;
|
|
893
|
+
|
|
894
|
+
if (this.scrollContainer.scrollHeight < this._scrollTop) {
|
|
895
|
+
this.scrollContent.style.minHeight = `${
|
|
896
|
+
this._scrollTop + window.innerHeight
|
|
897
|
+
}px`;
|
|
898
|
+
}
|
|
899
|
+
this.scrollContainer.scrollTop = this._scrollTop;
|
|
900
|
+
}
|
|
901
|
+
|
|
841
902
|
viewportHeight = 0;
|
|
842
903
|
|
|
843
904
|
currentTime = new Date();
|
|
@@ -890,8 +951,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
890
951
|
timeUpdateInterval: ReturnType<typeof setInterval> | null = null;
|
|
891
952
|
isExtendingRange = false; // Prevents concurrent range extensions
|
|
892
953
|
boundaryCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
954
|
+
wheelZoomSyncTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
893
955
|
lastWheelTime = 0; // Timestamp of last wheel event, used to distinguish wheel vs scrollbar drag
|
|
894
956
|
isCreatingEvent = false;
|
|
957
|
+
isPinchZooming = false;
|
|
958
|
+
pinchStartDistance = 0;
|
|
959
|
+
pinchStartHeight = MIN_DAY_HEIGHT;
|
|
960
|
+
pinchStartOriginY = 0;
|
|
895
961
|
eventCreationStart: { x: number; y: number } | null = null;
|
|
896
962
|
eventCreationEnd: { x: number; y: number } | null = null;
|
|
897
963
|
eventCreationShiftPressed = false;
|
|
@@ -912,28 +978,49 @@ export class CalendarViewElement extends LitElement {
|
|
|
912
978
|
resizingOriginalEnd: Date | null = null;
|
|
913
979
|
isResizingEvent = false;
|
|
914
980
|
|
|
915
|
-
_columnsPerRow = 7;
|
|
916
|
-
set columnsPerRow(value: number) {
|
|
917
|
-
const clamped = Math.max(1, Math.min(7, Math.floor(value)));
|
|
918
|
-
if (this._columnsPerRow !== clamped) {
|
|
919
|
-
this._columnsPerRow = clamped;
|
|
920
|
-
this.updateWeekOffsets();
|
|
921
|
-
this.renderCanvas();
|
|
922
|
-
this.requestUpdate();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
981
|
get columnsPerRow(): number {
|
|
926
|
-
return
|
|
982
|
+
return 7;
|
|
927
983
|
}
|
|
928
984
|
|
|
929
985
|
get rowsPerWeek(): number {
|
|
930
|
-
return
|
|
986
|
+
return 1;
|
|
931
987
|
}
|
|
932
988
|
|
|
933
989
|
getDayVisualPosition(dayIndex: number): { row: number; col: number } {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
990
|
+
return { row: 0, col: dayIndex };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
get scrollLeft(): number {
|
|
994
|
+
return this.scrollContainer?.scrollLeft ?? 0;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
getViewportGridWidth(): number {
|
|
998
|
+
if (!this.scrollContainer) return this.columnsPerRow * this.minDayColumnWidth;
|
|
999
|
+
return Math.max(
|
|
1000
|
+
0,
|
|
1001
|
+
this.scrollContainer.clientWidth - LEFT_GUTTER_WIDTH - MINIMAP_WIDTH,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
getGridWidth(): number {
|
|
1006
|
+
return Math.max(
|
|
1007
|
+
this.getViewportGridWidth(),
|
|
1008
|
+
this.columnsPerRow * this.minDayColumnWidth,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
getDayWidth(): number {
|
|
1013
|
+
return this.getGridWidth() / this.columnsPerRow;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
getContentWidth(): number {
|
|
1017
|
+
return LEFT_GUTTER_WIDTH + this.getGridWidth();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
getContentXFromClientX(clientX: number): number {
|
|
1021
|
+
if (!this.scrollContainer) return clientX;
|
|
1022
|
+
const rect = this.scrollContainer.getBoundingClientRect();
|
|
1023
|
+
return clientX - rect.left + this.scrollLeft;
|
|
937
1024
|
}
|
|
938
1025
|
|
|
939
1026
|
getVisualPositionFromCoords(
|
|
@@ -943,9 +1030,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
943
1030
|
): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
|
|
944
1031
|
if (!this.scrollContainer) return null;
|
|
945
1032
|
|
|
946
|
-
const dayWidth = gridWidth / this.
|
|
1033
|
+
const dayWidth = gridWidth / this.columnsPerRow;
|
|
947
1034
|
const col = Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth);
|
|
948
|
-
if (col < 0 || col >= this.
|
|
1035
|
+
if (col < 0 || col >= this.columnsPerRow) return null;
|
|
949
1036
|
|
|
950
1037
|
const week = this.weeks.find(
|
|
951
1038
|
(w) => w.height > 0 && y >= w.yOffset && y < w.yOffset + w.height,
|
|
@@ -954,7 +1041,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
954
1041
|
|
|
955
1042
|
const rowHeight = this.dayHeight;
|
|
956
1043
|
const row = Math.floor((y - week.yOffset) / rowHeight);
|
|
957
|
-
const dayIndex = row * this.
|
|
1044
|
+
const dayIndex = row * this.columnsPerRow + col;
|
|
958
1045
|
if (dayIndex < 0 || dayIndex > 6) return null;
|
|
959
1046
|
|
|
960
1047
|
const timeFraction = ((y - week.yOffset) % rowHeight) / rowHeight;
|
|
@@ -992,6 +1079,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
992
1079
|
height: number;
|
|
993
1080
|
} | null = null;
|
|
994
1081
|
isDescriptionExpanded = false;
|
|
1082
|
+
descriptionSummaryTargetKey: string | null = null;
|
|
1083
|
+
descriptionSummaryText = "";
|
|
1084
|
+
descriptionSummaryLoading = false;
|
|
1085
|
+
descriptionSummaryError: string | null = null;
|
|
1086
|
+
private requestedDescriptionSummaryKey: string | null = null;
|
|
995
1087
|
|
|
996
1088
|
notificationPopoverOpen = false;
|
|
997
1089
|
scheduledNotifications: any[] = [];
|
|
@@ -1037,7 +1129,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1037
1129
|
if (saved) {
|
|
1038
1130
|
return Math.max(
|
|
1039
1131
|
MIN_DAY_HEIGHT,
|
|
1040
|
-
Math.min(MAX_DAY_HEIGHT, parseFloat(saved)),
|
|
1132
|
+
Math.min(MAX_DAY_HEIGHT, Number.parseFloat(saved)),
|
|
1041
1133
|
);
|
|
1042
1134
|
}
|
|
1043
1135
|
|
|
@@ -1048,6 +1140,25 @@ export class CalendarViewElement extends LitElement {
|
|
|
1048
1140
|
localStorage.setItem("calendar-dayHeight", this.dayHeight.toString());
|
|
1049
1141
|
}
|
|
1050
1142
|
|
|
1143
|
+
loadMinDayColumnWidth(): number {
|
|
1144
|
+
const saved = localStorage.getItem("calendar-minDayColumnWidth");
|
|
1145
|
+
if (saved) {
|
|
1146
|
+
return Math.max(
|
|
1147
|
+
MIN_DAY_COLUMN_WIDTH,
|
|
1148
|
+
Math.min(MAX_DAY_COLUMN_WIDTH, Number.parseFloat(saved)),
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return DEFAULT_DAY_COLUMN_WIDTH;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
saveMinDayColumnWidth(): void {
|
|
1156
|
+
localStorage.setItem(
|
|
1157
|
+
"calendar-minDayColumnWidth",
|
|
1158
|
+
this.minDayColumnWidth.toString(),
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1051
1162
|
saveScrollPosition(): void {
|
|
1052
1163
|
// Save the date at the center of the viewport instead of pixel offset
|
|
1053
1164
|
const centerY = this.scrollTop + this.viewportHeight / 2;
|
|
@@ -1066,6 +1177,90 @@ export class CalendarViewElement extends LitElement {
|
|
|
1066
1177
|
}
|
|
1067
1178
|
}
|
|
1068
1179
|
|
|
1180
|
+
renderMonthLabels(
|
|
1181
|
+
ctx: CanvasRenderingContext2D,
|
|
1182
|
+
width: number,
|
|
1183
|
+
height: number,
|
|
1184
|
+
fontFamily: string,
|
|
1185
|
+
): void {
|
|
1186
|
+
const monthNames = [
|
|
1187
|
+
"January",
|
|
1188
|
+
"February",
|
|
1189
|
+
"March",
|
|
1190
|
+
"April",
|
|
1191
|
+
"May",
|
|
1192
|
+
"June",
|
|
1193
|
+
"July",
|
|
1194
|
+
"August",
|
|
1195
|
+
"September",
|
|
1196
|
+
"October",
|
|
1197
|
+
"November",
|
|
1198
|
+
"December",
|
|
1199
|
+
];
|
|
1200
|
+
const monthBoundaries: Array<{
|
|
1201
|
+
monthName: string;
|
|
1202
|
+
year: number;
|
|
1203
|
+
yOffset: number;
|
|
1204
|
+
}> = [];
|
|
1205
|
+
let previousMonthKey: string | null = null;
|
|
1206
|
+
|
|
1207
|
+
for (const week of this.weeks) {
|
|
1208
|
+
if (week.height === 0) continue;
|
|
1209
|
+
if (week.yOffset + week.height < this.scrollTop) continue;
|
|
1210
|
+
if (week.yOffset > this.scrollTop + this.viewportHeight) break;
|
|
1211
|
+
|
|
1212
|
+
for (const day of week.days) {
|
|
1213
|
+
if (!day) continue;
|
|
1214
|
+
const monthIndex = day.getMonth();
|
|
1215
|
+
const year = day.getFullYear();
|
|
1216
|
+
const monthKey = `${monthIndex}-${year}`;
|
|
1217
|
+
if (monthKey === previousMonthKey) continue;
|
|
1218
|
+
|
|
1219
|
+
previousMonthKey = monthKey;
|
|
1220
|
+
const monthName = monthNames[monthIndex];
|
|
1221
|
+
if (!monthName) continue;
|
|
1222
|
+
monthBoundaries.push({ monthName, year, yOffset: week.yOffset });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const bgPrimary =
|
|
1227
|
+
getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
|
|
1228
|
+
"rgba(30, 30, 30, 1)";
|
|
1229
|
+
const monthTextPrimary =
|
|
1230
|
+
getComputedStyle(this).getPropertyValue("--text-primary").trim() ||
|
|
1231
|
+
"rgba(255, 255, 255, 0.95)";
|
|
1232
|
+
|
|
1233
|
+
for (let i = 0; i < monthBoundaries.length; i++) {
|
|
1234
|
+
const month = monthBoundaries[i];
|
|
1235
|
+
if (!month) continue;
|
|
1236
|
+
const nextMonth = monthBoundaries[i + 1];
|
|
1237
|
+
const nextMonthY = nextMonth
|
|
1238
|
+
? nextMonth.yOffset
|
|
1239
|
+
: Number.POSITIVE_INFINITY;
|
|
1240
|
+
|
|
1241
|
+
if (nextMonthY < this.scrollTop) continue;
|
|
1242
|
+
if (month.yOffset > this.scrollTop + this.viewportHeight) break;
|
|
1243
|
+
|
|
1244
|
+
const stickyTop = Math.max(0, this.scrollTop - month.yOffset);
|
|
1245
|
+
const maxStickyTop = nextMonthY - month.yOffset - 24;
|
|
1246
|
+
const clampedStickyTop = Math.min(stickyTop, maxStickyTop);
|
|
1247
|
+
const finalTop = month.yOffset + clampedStickyTop - this.scrollTop;
|
|
1248
|
+
const isSticky = clampedStickyTop > 0;
|
|
1249
|
+
|
|
1250
|
+
ctx.save();
|
|
1251
|
+
ctx.globalAlpha = isSticky ? 1 : 0.5;
|
|
1252
|
+
ctx.fillStyle = bgPrimary;
|
|
1253
|
+
ctx.fillRect(0, finalTop, width, 40);
|
|
1254
|
+
ctx.globalAlpha = 1;
|
|
1255
|
+
ctx.font = `bold 18px ${fontFamily}`;
|
|
1256
|
+
ctx.textAlign = "left";
|
|
1257
|
+
ctx.textBaseline = "top";
|
|
1258
|
+
ctx.fillStyle = monthTextPrimary;
|
|
1259
|
+
ctx.fillText(`${month.monthName} ${month.year}`, 20, finalTop + 10);
|
|
1260
|
+
ctx.restore();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1069
1264
|
loadScrollPosition(): void {
|
|
1070
1265
|
const saved = localStorage.getItem("calendar-scrollDate");
|
|
1071
1266
|
if (saved) {
|
|
@@ -1099,9 +1294,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
1099
1294
|
CalendarInternal.isSameDay(d, now),
|
|
1100
1295
|
);
|
|
1101
1296
|
if (todayIndex >= 0) {
|
|
1102
|
-
const row = Math.floor(todayIndex / this._columnsPerRow);
|
|
1103
1297
|
const timeFraction = (now.getHours() + now.getMinutes() / 60) / 24;
|
|
1104
|
-
offsetInWeek =
|
|
1298
|
+
offsetInWeek = timeFraction;
|
|
1105
1299
|
}
|
|
1106
1300
|
}
|
|
1107
1301
|
|
|
@@ -1328,8 +1522,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
1328
1522
|
window.removeEventListener("paste", this.onPaste);
|
|
1329
1523
|
window.removeEventListener("keydown", this.onKeyDown);
|
|
1330
1524
|
window.removeEventListener("keyup", this.onKeyUp);
|
|
1525
|
+
if (this.wheelZoomSyncTimeout) {
|
|
1526
|
+
clearTimeout(this.wheelZoomSyncTimeout);
|
|
1527
|
+
this.wheelZoomSyncTimeout = null;
|
|
1528
|
+
}
|
|
1331
1529
|
|
|
1332
1530
|
if (this.scrollContainer) {
|
|
1531
|
+
this.scrollContainer.removeEventListener("scroll", this.onScroll);
|
|
1532
|
+
this.scrollContainer.removeEventListener("touchstart", this.onTouchStart);
|
|
1533
|
+
this.scrollContainer.removeEventListener("touchmove", this.onTouchMove);
|
|
1534
|
+
this.scrollContainer.removeEventListener("touchend", this.onTouchEnd);
|
|
1535
|
+
this.scrollContainer.removeEventListener("touchcancel", this.onTouchEnd);
|
|
1333
1536
|
this.scrollContainer.removeEventListener(
|
|
1334
1537
|
"mouseleave",
|
|
1335
1538
|
this.onScrollContainerMouseLeave,
|
|
@@ -1359,21 +1562,141 @@ export class CalendarViewElement extends LitElement {
|
|
|
1359
1562
|
this.repositionEventDetailOverlay();
|
|
1360
1563
|
}
|
|
1361
1564
|
|
|
1565
|
+
clearDescriptionSummary(): void {
|
|
1566
|
+
if (this.requestedDescriptionSummaryKey) {
|
|
1567
|
+
this.dispatchEvent(
|
|
1568
|
+
new CustomEvent("cancel-description-summary", {
|
|
1569
|
+
detail: { key: this.requestedDescriptionSummaryKey },
|
|
1570
|
+
bubbles: true,
|
|
1571
|
+
composed: true,
|
|
1572
|
+
}),
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
this.requestedDescriptionSummaryKey = null;
|
|
1576
|
+
this.descriptionSummaryTargetKey = null;
|
|
1577
|
+
this.descriptionSummaryText = "";
|
|
1578
|
+
this.descriptionSummaryLoading = false;
|
|
1579
|
+
this.descriptionSummaryError = null;
|
|
1580
|
+
this.requestUpdate();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private getDescriptionSummaryTargetKey(event: CalendarEvent): string {
|
|
1584
|
+
return `${event.id}:${sanitizeEventDescription(event.description ?? "")}`;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
private requestDescriptionSummary(event: CalendarEvent): void {
|
|
1588
|
+
const description = sanitizeEventDescription(
|
|
1589
|
+
event.description ?? "",
|
|
1590
|
+
).trim();
|
|
1591
|
+
if (description.length <= 200) return;
|
|
1592
|
+
|
|
1593
|
+
const targetKey = this.getDescriptionSummaryTargetKey(event);
|
|
1594
|
+
this.clearDescriptionSummary();
|
|
1595
|
+
this.requestedDescriptionSummaryKey = targetKey;
|
|
1596
|
+
this.descriptionSummaryTargetKey = targetKey;
|
|
1597
|
+
this.descriptionSummaryText = "";
|
|
1598
|
+
this.descriptionSummaryLoading = true;
|
|
1599
|
+
this.descriptionSummaryError = null;
|
|
1600
|
+
this.dispatchEvent(
|
|
1601
|
+
new CustomEvent("request-description-summary", {
|
|
1602
|
+
detail: {
|
|
1603
|
+
event,
|
|
1604
|
+
key: targetKey,
|
|
1605
|
+
description,
|
|
1606
|
+
},
|
|
1607
|
+
bubbles: true,
|
|
1608
|
+
composed: true,
|
|
1609
|
+
}),
|
|
1610
|
+
);
|
|
1611
|
+
this.requestUpdate();
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
startDescriptionSummary(key: string): void {
|
|
1615
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1616
|
+
this.descriptionSummaryText = "";
|
|
1617
|
+
this.descriptionSummaryLoading = true;
|
|
1618
|
+
this.descriptionSummaryError = null;
|
|
1619
|
+
this.requestUpdate();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
appendDescriptionSummaryChunk(key: string, chunk: string): void {
|
|
1623
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1624
|
+
if (!chunk) return;
|
|
1625
|
+
this.descriptionSummaryText += chunk;
|
|
1626
|
+
this.descriptionSummaryLoading = true;
|
|
1627
|
+
this.requestUpdate();
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
finishDescriptionSummary(key: string, error: string | null = null): void {
|
|
1631
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1632
|
+
this.descriptionSummaryLoading = false;
|
|
1633
|
+
this.descriptionSummaryError = error;
|
|
1634
|
+
this.requestUpdate();
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
failDescriptionSummary(key: string, message: string): void {
|
|
1638
|
+
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1639
|
+
this.descriptionSummaryLoading = false;
|
|
1640
|
+
this.descriptionSummaryError = message;
|
|
1641
|
+
if (
|
|
1642
|
+
!this.descriptionSummaryText &&
|
|
1643
|
+
this.selectedEventForDetail?.description
|
|
1644
|
+
) {
|
|
1645
|
+
this.descriptionSummaryText =
|
|
1646
|
+
this.selectedEventForDetail.description.replaceAll("\\n", "\n");
|
|
1647
|
+
}
|
|
1648
|
+
this.requestUpdate();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
cancelDescriptionSummary(key: string): void {
|
|
1652
|
+
if (key !== this.descriptionSummaryTargetKey) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
this.descriptionSummaryLoading = false;
|
|
1656
|
+
this.requestUpdate();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1362
1659
|
repositionEventDetailOverlay(): void {
|
|
1363
1660
|
if (!this.selectedEventForDetail || !this.selectedEventRect) return;
|
|
1364
1661
|
|
|
1365
|
-
const overlay = this.renderRoot.querySelector<HTMLElement>(
|
|
1662
|
+
const overlay = this.renderRoot.querySelector<HTMLElement>(
|
|
1663
|
+
".event-detail-overlay",
|
|
1664
|
+
);
|
|
1366
1665
|
if (!overlay) return;
|
|
1666
|
+
const currentRect = this.eventRects.find(
|
|
1667
|
+
(rect) => rect.event === this.selectedEventForDetail,
|
|
1668
|
+
);
|
|
1669
|
+
if (currentRect) {
|
|
1670
|
+
this.selectedEventRect = {
|
|
1671
|
+
x: currentRect.x,
|
|
1672
|
+
y: currentRect.y,
|
|
1673
|
+
width: currentRect.width,
|
|
1674
|
+
height: currentRect.height,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1367
1677
|
|
|
1678
|
+
const actualWidth = overlay.offsetWidth;
|
|
1368
1679
|
const actualHeight = overlay.offsetHeight;
|
|
1369
1680
|
const GAP = 8;
|
|
1681
|
+
const containerWidth = this.scrollContainer?.clientWidth || 800;
|
|
1370
1682
|
const containerHeight = this.scrollContainer?.clientHeight || 600;
|
|
1371
1683
|
|
|
1684
|
+
const rightPosition =
|
|
1685
|
+
this.selectedEventRect.x + this.selectedEventRect.width + GAP;
|
|
1686
|
+
const leftPosition = this.selectedEventRect.x - actualWidth - GAP;
|
|
1687
|
+
const preferredLeft =
|
|
1688
|
+
rightPosition + actualWidth <= containerWidth
|
|
1689
|
+
? rightPosition
|
|
1690
|
+
: leftPosition;
|
|
1691
|
+
const minLeft = LEFT_GUTTER_WIDTH + GAP;
|
|
1692
|
+
const maxLeft = containerWidth - actualWidth - GAP;
|
|
1693
|
+
const left = Math.max(minLeft, Math.min(maxLeft, preferredLeft));
|
|
1372
1694
|
const rawTop = this.selectedEventRect.y - this.scrollTop;
|
|
1373
1695
|
const minTop = GAP;
|
|
1374
1696
|
const maxTop = containerHeight - actualHeight - GAP;
|
|
1375
1697
|
const top = Math.max(minTop, Math.min(maxTop, rawTop));
|
|
1376
1698
|
|
|
1699
|
+
overlay.style.left = `${left}px`;
|
|
1377
1700
|
overlay.style.top = `${top}px`;
|
|
1378
1701
|
}
|
|
1379
1702
|
|
|
@@ -1390,14 +1713,24 @@ export class CalendarViewElement extends LitElement {
|
|
|
1390
1713
|
|
|
1391
1714
|
const self = this;
|
|
1392
1715
|
const eventsState: EventsState = {
|
|
1393
|
-
get events() {
|
|
1394
|
-
|
|
1716
|
+
get events() {
|
|
1717
|
+
return self.events;
|
|
1718
|
+
},
|
|
1719
|
+
get previewEvents() {
|
|
1720
|
+
return self.getPreviewEvents();
|
|
1721
|
+
},
|
|
1722
|
+
get hoveredEventId() {
|
|
1723
|
+
return self.hoveredEventId;
|
|
1724
|
+
},
|
|
1395
1725
|
isEventSelected: (event) => self.internal.isEventSelected(event),
|
|
1396
|
-
shouldRenderEventWithStripes: (event) =>
|
|
1726
|
+
shouldRenderEventWithStripes: (event) =>
|
|
1727
|
+
self.shouldRenderEventWithStripes(event),
|
|
1397
1728
|
getStripePatternCanvas: () => self.getStripePatternCanvas(),
|
|
1398
1729
|
};
|
|
1399
1730
|
const heatmapState = {
|
|
1400
|
-
get events() {
|
|
1731
|
+
get events() {
|
|
1732
|
+
return self.heatmapEvents;
|
|
1733
|
+
},
|
|
1401
1734
|
};
|
|
1402
1735
|
this.eventsLayer = createEventsLayer(eventsState);
|
|
1403
1736
|
this.layers = [
|
|
@@ -1410,14 +1743,30 @@ export class CalendarViewElement extends LitElement {
|
|
|
1410
1743
|
|
|
1411
1744
|
// Restore zoom level from localStorage
|
|
1412
1745
|
const savedDayHeight = this.loadDayHeight();
|
|
1413
|
-
if (savedDayHeight !==
|
|
1746
|
+
if (savedDayHeight !== MIN_DAY_HEIGHT) {
|
|
1414
1747
|
this.dayHeight = savedDayHeight;
|
|
1415
1748
|
}
|
|
1749
|
+
const savedMinDayColumnWidth = this.loadMinDayColumnWidth();
|
|
1750
|
+
if (savedMinDayColumnWidth !== DEFAULT_DAY_COLUMN_WIDTH) {
|
|
1751
|
+
this.minDayColumnWidth = savedMinDayColumnWidth;
|
|
1752
|
+
}
|
|
1416
1753
|
|
|
1417
1754
|
if (this.scrollContainer) {
|
|
1418
1755
|
this.scrollContainer.addEventListener("scroll", this.onScroll, {
|
|
1419
1756
|
passive: false,
|
|
1420
1757
|
});
|
|
1758
|
+
this.scrollContainer.addEventListener("touchstart", this.onTouchStart, {
|
|
1759
|
+
passive: false,
|
|
1760
|
+
});
|
|
1761
|
+
this.scrollContainer.addEventListener("touchmove", this.onTouchMove, {
|
|
1762
|
+
passive: false,
|
|
1763
|
+
});
|
|
1764
|
+
this.scrollContainer.addEventListener("touchend", this.onTouchEnd, {
|
|
1765
|
+
passive: false,
|
|
1766
|
+
});
|
|
1767
|
+
this.scrollContainer.addEventListener("touchcancel", this.onTouchEnd, {
|
|
1768
|
+
passive: false,
|
|
1769
|
+
});
|
|
1421
1770
|
this.scrollContainer.addEventListener(
|
|
1422
1771
|
"mouseleave",
|
|
1423
1772
|
this.onScrollContainerMouseLeave,
|
|
@@ -1529,30 +1878,27 @@ export class CalendarViewElement extends LitElement {
|
|
|
1529
1878
|
|
|
1530
1879
|
updateWeekOffsets(): void {
|
|
1531
1880
|
let y = 0;
|
|
1532
|
-
const weekHeight = this.dayHeight
|
|
1881
|
+
const weekHeight = this.dayHeight;
|
|
1533
1882
|
|
|
1534
1883
|
if (this.filter) {
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1884
|
+
const eventRanges = this.getFilteredEventRanges(this.events);
|
|
1885
|
+
this.ensureFilteredContextWeeks(eventRanges);
|
|
1886
|
+
|
|
1887
|
+
const visibleWeekIndexes = new Set<number>();
|
|
1888
|
+
for (const index of this.getMatchingWeekIndexes(eventRanges)) {
|
|
1889
|
+
for (
|
|
1890
|
+
let visibleIndex = Math.max(0, index - FILTER_CONTEXT_WEEKS);
|
|
1891
|
+
visibleIndex <=
|
|
1892
|
+
Math.min(this.weeks.length - 1, index + FILTER_CONTEXT_WEEKS);
|
|
1893
|
+
visibleIndex++
|
|
1894
|
+
) {
|
|
1895
|
+
visibleWeekIndexes.add(visibleIndex);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1542
1898
|
|
|
1543
|
-
for (const week of this.weeks) {
|
|
1899
|
+
for (const [index, week] of this.weeks.entries()) {
|
|
1544
1900
|
week.yOffset = y;
|
|
1545
|
-
|
|
1546
|
-
// Check if any day in this week overlaps any event range
|
|
1547
|
-
const weekStartTime = week.days[0]?.getTime() ?? 0;
|
|
1548
|
-
const weekEndTime = week.days[6]?.getTime() ?? 0;
|
|
1549
|
-
|
|
1550
|
-
// Quick check: skip if week is entirely outside all event ranges
|
|
1551
|
-
const hasEvents = eventRanges.some(
|
|
1552
|
-
(range) => range.end >= weekStartTime && range.start <= weekEndTime,
|
|
1553
|
-
);
|
|
1554
|
-
|
|
1555
|
-
week.height = hasEvents ? weekHeight : 0;
|
|
1901
|
+
week.height = visibleWeekIndexes.has(index) ? weekHeight : 0;
|
|
1556
1902
|
y += week.height;
|
|
1557
1903
|
}
|
|
1558
1904
|
} else {
|
|
@@ -1566,6 +1912,77 @@ export class CalendarViewElement extends LitElement {
|
|
|
1566
1912
|
this.totalHeight = y;
|
|
1567
1913
|
}
|
|
1568
1914
|
|
|
1915
|
+
private getFilteredEventRanges(
|
|
1916
|
+
events: CalendarEvent[],
|
|
1917
|
+
): Array<{ start: number; end: number }> {
|
|
1918
|
+
return events
|
|
1919
|
+
.map((event) => ({
|
|
1920
|
+
start: CalendarInternal.startOfDayTime(event.start),
|
|
1921
|
+
end: CalendarInternal.endOfDayTime(event.end),
|
|
1922
|
+
}))
|
|
1923
|
+
.sort((a, b) => a.start - b.start);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
private getMatchingWeekIndexes(
|
|
1927
|
+
eventRanges: Array<{ start: number; end: number }>,
|
|
1928
|
+
): number[] {
|
|
1929
|
+
const matchingIndexes: number[] = [];
|
|
1930
|
+
|
|
1931
|
+
for (const [index, week] of this.weeks.entries()) {
|
|
1932
|
+
const weekStartTime = CalendarInternal.startOfDayTime(
|
|
1933
|
+
week.days[0] ?? new Date(0),
|
|
1934
|
+
);
|
|
1935
|
+
const weekEndTime = CalendarInternal.endOfDayTime(
|
|
1936
|
+
week.days[6] ?? new Date(0),
|
|
1937
|
+
);
|
|
1938
|
+
const hasEvents = eventRanges.some(
|
|
1939
|
+
(range) => range.end >= weekStartTime && range.start <= weekEndTime,
|
|
1940
|
+
);
|
|
1941
|
+
if (hasEvents) {
|
|
1942
|
+
matchingIndexes.push(index);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
return matchingIndexes;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
private ensureFilteredContextWeeks(
|
|
1950
|
+
eventRanges: Array<{ start: number; end: number }>,
|
|
1951
|
+
): void {
|
|
1952
|
+
if (eventRanges.length === 0) return;
|
|
1953
|
+
|
|
1954
|
+
let matchingWeekIndexes = this.getMatchingWeekIndexes(eventRanges);
|
|
1955
|
+
if (matchingWeekIndexes.length === 0) {
|
|
1956
|
+
this.weeks = this.internal.resetRangeAroundDate(
|
|
1957
|
+
new Date(eventRanges[0]?.start),
|
|
1958
|
+
);
|
|
1959
|
+
matchingWeekIndexes = this.getMatchingWeekIndexes(eventRanges);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
while (
|
|
1963
|
+
matchingWeekIndexes.length > 0 &&
|
|
1964
|
+
matchingWeekIndexes[0]! < FILTER_CONTEXT_WEEKS
|
|
1965
|
+
) {
|
|
1966
|
+
const newWeeks = this.internal.extendRange("past");
|
|
1967
|
+
if (newWeeks.length === 0) break;
|
|
1968
|
+
this.weeks = [...newWeeks, ...this.weeks];
|
|
1969
|
+
matchingWeekIndexes = this.getMatchingWeekIndexes(eventRanges);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
while (
|
|
1973
|
+
matchingWeekIndexes.length > 0 &&
|
|
1974
|
+
this.weeks.length -
|
|
1975
|
+
1 -
|
|
1976
|
+
matchingWeekIndexes[matchingWeekIndexes.length - 1]! <
|
|
1977
|
+
FILTER_CONTEXT_WEEKS
|
|
1978
|
+
) {
|
|
1979
|
+
const newWeeks = this.internal.extendRange("future");
|
|
1980
|
+
if (newWeeks.length === 0) break;
|
|
1981
|
+
this.weeks = [...this.weeks, ...newWeeks];
|
|
1982
|
+
matchingWeekIndexes = this.getMatchingWeekIndexes(eventRanges);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1569
1986
|
handleResize(): void {
|
|
1570
1987
|
if (!this.canvas || !this.scrollContainer) return;
|
|
1571
1988
|
|
|
@@ -1574,8 +1991,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
1574
1991
|
|
|
1575
1992
|
this.rect = rect;
|
|
1576
1993
|
|
|
1577
|
-
|
|
1994
|
+
const canvasWidth = LEFT_GUTTER_WIDTH + this.getViewportGridWidth();
|
|
1995
|
+
this.canvas.width = canvasWidth * dpr;
|
|
1578
1996
|
this.canvas.height = rect.height * dpr;
|
|
1997
|
+
this.canvas.style.width = `${canvasWidth}px`;
|
|
1998
|
+
this.canvas.style.height = `${rect.height}px`;
|
|
1579
1999
|
this.viewportHeight = rect.height;
|
|
1580
2000
|
|
|
1581
2001
|
// Reset and rescale context (scale is reset when canvas dimensions change)
|
|
@@ -1586,7 +2006,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1586
2006
|
|
|
1587
2007
|
// Resize and configure overlay canvas
|
|
1588
2008
|
if (this.overlayCanvas) {
|
|
1589
|
-
const overlayWidth =
|
|
2009
|
+
const overlayWidth = this.getViewportGridWidth();
|
|
1590
2010
|
this.overlayCanvas.width = overlayWidth * dpr;
|
|
1591
2011
|
this.overlayCanvas.height = rect.height * dpr;
|
|
1592
2012
|
this.overlayCanvas.style.width = `${overlayWidth}px`;
|
|
@@ -1598,30 +2018,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1598
2018
|
}
|
|
1599
2019
|
}
|
|
1600
2020
|
|
|
1601
|
-
this.updateColumnsForViewport();
|
|
1602
2021
|
this.renderCanvas();
|
|
1603
2022
|
}
|
|
1604
2023
|
|
|
1605
|
-
updateColumnsForViewport(): void {
|
|
1606
|
-
if (!this.scrollContainer) return;
|
|
1607
|
-
|
|
1608
|
-
const rect = this.scrollContainer.getBoundingClientRect();
|
|
1609
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
1610
|
-
|
|
1611
|
-
// Determine optimal columns based on available width
|
|
1612
|
-
// Minimum 60px per day column for usability
|
|
1613
|
-
const minDayWidth = 120;
|
|
1614
|
-
const optimalColumns = Math.max(
|
|
1615
|
-
1,
|
|
1616
|
-
Math.min(7, Math.floor(gridWidth / minDayWidth)),
|
|
1617
|
-
);
|
|
1618
|
-
|
|
1619
|
-
if (this._columnsPerRow !== optimalColumns) {
|
|
1620
|
-
this._columnsPerRow = optimalColumns;
|
|
1621
|
-
this.updateWeekOffsets();
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
2024
|
resolveStyles(): Record<string, string> {
|
|
1626
2025
|
const cs = getComputedStyle(this);
|
|
1627
2026
|
const props = [
|
|
@@ -1651,7 +2050,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
1651
2050
|
const ids = new Set<string>();
|
|
1652
2051
|
if (saved) {
|
|
1653
2052
|
try {
|
|
1654
|
-
const sources = JSON.parse(saved) as Array<{
|
|
2053
|
+
const sources = JSON.parse(saved) as Array<{
|
|
2054
|
+
id?: string;
|
|
2055
|
+
type?: string;
|
|
2056
|
+
}>;
|
|
1655
2057
|
for (const source of sources) {
|
|
1656
2058
|
if (source?.type === "timeseries-json" && source.id) {
|
|
1657
2059
|
ids.add(source.id);
|
|
@@ -1692,7 +2094,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1692
2094
|
const timeseriesIds = this.loadTimeseriesSourceIds();
|
|
1693
2095
|
const enabledKey = [...this.internal.enabledCalendars].join(",");
|
|
1694
2096
|
const lockedKey = [...this.internal.lockedCalendars].join(",");
|
|
1695
|
-
const key = `${start.toISOString()}::${end.toISOString()}::${
|
|
2097
|
+
const key = `${start.toISOString()}::${end.toISOString()}::${
|
|
2098
|
+
this.filter || ""
|
|
2099
|
+
}::${enabledKey}::${lockedKey}`;
|
|
1696
2100
|
if (!force && key === this.heatmapQueryKey) return;
|
|
1697
2101
|
this.heatmapQueryKey = key;
|
|
1698
2102
|
|
|
@@ -1728,8 +2132,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1728
2132
|
ctx.clearRect(0, 0, width, height);
|
|
1729
2133
|
|
|
1730
2134
|
const scrollTop = this.scrollTop;
|
|
1731
|
-
const
|
|
1732
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
2135
|
+
const dayWidth = this.getDayWidth();
|
|
1733
2136
|
|
|
1734
2137
|
const visibleWeeks = this.weeks.filter(
|
|
1735
2138
|
(w) =>
|
|
@@ -1745,10 +2148,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
1745
2148
|
width,
|
|
1746
2149
|
height,
|
|
1747
2150
|
scrollTop,
|
|
2151
|
+
scrollLeft: this.scrollLeft,
|
|
1748
2152
|
dayWidth,
|
|
1749
2153
|
dayHeight: this.dayHeight,
|
|
1750
2154
|
leftGutterWidth: LEFT_GUTTER_WIDTH,
|
|
1751
|
-
columnsPerRow: this.
|
|
2155
|
+
columnsPerRow: this.columnsPerRow,
|
|
1752
2156
|
rowsPerWeek: this.rowsPerWeek,
|
|
1753
2157
|
visibleWeeks,
|
|
1754
2158
|
allWeeks: this.weeks,
|
|
@@ -1765,6 +2169,14 @@ export class CalendarViewElement extends LitElement {
|
|
|
1765
2169
|
ctx.restore();
|
|
1766
2170
|
}
|
|
1767
2171
|
|
|
2172
|
+
this.renderTimeScaleGutter(
|
|
2173
|
+
ctx,
|
|
2174
|
+
visibleWeeks,
|
|
2175
|
+
height,
|
|
2176
|
+
fontFamily,
|
|
2177
|
+
lc.styles,
|
|
2178
|
+
);
|
|
2179
|
+
|
|
1768
2180
|
// Copy event rects from events layer for hit-testing
|
|
1769
2181
|
if (this.eventsLayer) {
|
|
1770
2182
|
this.eventRects = this.eventsLayer.eventRects;
|
|
@@ -1777,14 +2189,102 @@ export class CalendarViewElement extends LitElement {
|
|
|
1777
2189
|
// Draw sticky weekday labels at top of viewport
|
|
1778
2190
|
this.renderWeekdayLabels(ctx, dayWidth, visibleWeeks, scrollTop, height);
|
|
1779
2191
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2192
|
+
this.renderMinimap();
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
renderTimeScaleGutter(
|
|
2196
|
+
ctx: CanvasRenderingContext2D,
|
|
2197
|
+
visibleWeeks: WeekInfo[],
|
|
2198
|
+
height: number,
|
|
2199
|
+
fontFamily: string,
|
|
2200
|
+
styles: Record<string, string>,
|
|
2201
|
+
): void {
|
|
2202
|
+
if (visibleWeeks.length === 0) return;
|
|
2203
|
+
|
|
2204
|
+
const hourLabelOpacity = Math.max(
|
|
2205
|
+
0,
|
|
2206
|
+
Math.min(1, (this.dayHeight - TIME_SCALE_DAY_HEIGHT) / 300),
|
|
2207
|
+
);
|
|
2208
|
+
if (hourLabelOpacity <= 0.1) return;
|
|
2209
|
+
|
|
2210
|
+
const textMuted = styles["--text-muted"] || "rgba(255, 255, 255, 0.4)";
|
|
2211
|
+
const textPrimary = styles["--text-primary"] || "rgba(255, 255, 255, 1)";
|
|
2212
|
+
const bgPrimary = styles["--bg-primary"] || "rgba(30, 30, 30, 0.9)";
|
|
2213
|
+
const bgElevated = styles["--bg-elevated"] || "rgba(0, 0, 0, 0.7)";
|
|
2214
|
+
const today = new Date();
|
|
2215
|
+
|
|
2216
|
+
ctx.save();
|
|
2217
|
+
ctx.font = `500 11px ${fontFamily}`;
|
|
2218
|
+
ctx.textBaseline = "bottom";
|
|
2219
|
+
ctx.textAlign = "right";
|
|
2220
|
+
ctx.fillStyle = textMuted.replace(
|
|
2221
|
+
/[\d.]+\)$/,
|
|
2222
|
+
`${0.4 * hourLabelOpacity})`,
|
|
2223
|
+
);
|
|
2224
|
+
|
|
2225
|
+
for (const week of visibleWeeks) {
|
|
2226
|
+
const y = week.yOffset - this.scrollTop;
|
|
2227
|
+
const visibleWeekTop = Math.max(0, y);
|
|
2228
|
+
const visibleWeekBottom = Math.min(height, y + week.height);
|
|
2229
|
+
|
|
2230
|
+
if (visibleWeekBottom > visibleWeekTop) {
|
|
2231
|
+
ctx.save();
|
|
2232
|
+
ctx.globalAlpha = 0.7 * hourLabelOpacity;
|
|
2233
|
+
ctx.fillStyle = bgPrimary;
|
|
2234
|
+
ctx.fillRect(
|
|
2235
|
+
0,
|
|
2236
|
+
visibleWeekTop,
|
|
2237
|
+
LEFT_GUTTER_WIDTH - 1,
|
|
2238
|
+
visibleWeekBottom - visibleWeekTop,
|
|
2239
|
+
);
|
|
2240
|
+
ctx.restore();
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
2244
|
+
const hourY = y + (hour / 24) * this.dayHeight;
|
|
2245
|
+
if (hourY < 0 || hourY > height) continue;
|
|
2246
|
+
const label = `${hour.toString().padStart(2, "0")}:00`;
|
|
2247
|
+
ctx.fillText(label, 48, hourY + 4);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const todayIndex = week.days.findIndex((d) =>
|
|
2251
|
+
CalendarInternal.isSameDay(d, today),
|
|
2252
|
+
);
|
|
2253
|
+
if (todayIndex < 0) continue;
|
|
2254
|
+
|
|
2255
|
+
const currentMinutes = today.getHours() * 60 + today.getMinutes();
|
|
2256
|
+
const timeY = y + (currentMinutes / 1440) * this.dayHeight;
|
|
2257
|
+
if (timeY < 0 || timeY > height) continue;
|
|
2258
|
+
|
|
2259
|
+
const hours = today.getHours().toString().padStart(2, "0");
|
|
2260
|
+
const minutes = today.getMinutes().toString().padStart(2, "0");
|
|
2261
|
+
const timeText = `${hours}:${minutes}`;
|
|
2262
|
+
const textWidth = ctx.measureText(timeText).width;
|
|
2263
|
+
const bgPaddingX = 6;
|
|
2264
|
+
const textX = 48;
|
|
2265
|
+
|
|
2266
|
+
ctx.fillStyle = bgElevated;
|
|
2267
|
+
ctx.beginPath();
|
|
2268
|
+
ctx.roundRect(
|
|
2269
|
+
textX - textWidth - bgPaddingX,
|
|
2270
|
+
timeY - 8,
|
|
2271
|
+
textWidth + bgPaddingX * 2,
|
|
2272
|
+
16,
|
|
2273
|
+
4,
|
|
2274
|
+
);
|
|
2275
|
+
ctx.fill();
|
|
2276
|
+
|
|
2277
|
+
ctx.fillStyle = textPrimary;
|
|
2278
|
+
ctx.textBaseline = "middle";
|
|
2279
|
+
ctx.fillText(timeText, textX, timeY);
|
|
2280
|
+
ctx.textBaseline = "bottom";
|
|
2281
|
+
ctx.fillStyle = textMuted.replace(
|
|
2282
|
+
/[\d.]+\)$/,
|
|
2283
|
+
`${0.4 * hourLabelOpacity})`,
|
|
2284
|
+
);
|
|
1785
2285
|
}
|
|
1786
2286
|
|
|
1787
|
-
|
|
2287
|
+
ctx.restore();
|
|
1788
2288
|
}
|
|
1789
2289
|
|
|
1790
2290
|
toggleLayer(name: string): void {
|
|
@@ -1794,7 +2294,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
1794
2294
|
this.renderCanvas();
|
|
1795
2295
|
}
|
|
1796
2296
|
|
|
1797
|
-
|
|
1798
2297
|
onWheel = (e: WheelEvent): void => {
|
|
1799
2298
|
if (this.hasAttribute("scroll-lock")) return;
|
|
1800
2299
|
this.lastWheelTime = Date.now();
|
|
@@ -1804,7 +2303,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1804
2303
|
}
|
|
1805
2304
|
|
|
1806
2305
|
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
1807
|
-
const
|
|
2306
|
+
const isPinchZoomGesture = e.ctrlKey;
|
|
2307
|
+
const isShortcutZoom = isMac ? e.metaKey : e.ctrlKey;
|
|
2308
|
+
const isZoomGesture = isPinchZoomGesture || isShortcutZoom;
|
|
1808
2309
|
|
|
1809
2310
|
if (this.isFiltered && this.historyIndex === 0) {
|
|
1810
2311
|
this.replaceFilterInHistory();
|
|
@@ -1812,7 +2313,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1812
2313
|
this.debouncedSaveToHistory();
|
|
1813
2314
|
}
|
|
1814
2315
|
|
|
1815
|
-
if (!
|
|
2316
|
+
if (!isZoomGesture || !this.scrollContainer) {
|
|
1816
2317
|
this.updateMousePosition();
|
|
1817
2318
|
return;
|
|
1818
2319
|
}
|
|
@@ -1832,8 +2333,95 @@ export class CalendarViewElement extends LitElement {
|
|
|
1832
2333
|
const newOriginY = this.zoomOriginY * scaleRatio;
|
|
1833
2334
|
const newScrollTop = newOriginY - this.zoomViewportY;
|
|
1834
2335
|
|
|
1835
|
-
|
|
2336
|
+
// Avoid DOM scroll sync during wheel zoom to prevent scroll/zoom race flicker.
|
|
2337
|
+
this.setView(newHeight, newScrollTop, false);
|
|
1836
2338
|
this.zoomOriginY = newOriginY;
|
|
2339
|
+
if (this.wheelZoomSyncTimeout) {
|
|
2340
|
+
clearTimeout(this.wheelZoomSyncTimeout);
|
|
2341
|
+
}
|
|
2342
|
+
this.wheelZoomSyncTimeout = setTimeout(() => {
|
|
2343
|
+
this.syncScrollDomToState();
|
|
2344
|
+
this.wheelZoomSyncTimeout = null;
|
|
2345
|
+
}, 80);
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
getTouchDistance(touches: TouchList): number {
|
|
2349
|
+
if (touches.length < 2) return 0;
|
|
2350
|
+
const first = touches[0];
|
|
2351
|
+
const second = touches[1];
|
|
2352
|
+
if (!first || !second) return 0;
|
|
2353
|
+
return Math.hypot(
|
|
2354
|
+
second.clientX - first.clientX,
|
|
2355
|
+
second.clientY - first.clientY,
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
getTouchCenterY(touches: TouchList): number {
|
|
2360
|
+
if (touches.length < 2) return 0;
|
|
2361
|
+
const first = touches[0];
|
|
2362
|
+
const second = touches[1];
|
|
2363
|
+
if (!first || !second || !this.scrollContainer) return 0;
|
|
2364
|
+
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2365
|
+
return (first.clientY + second.clientY) / 2 - rect.top;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
onTouchStart = (e: TouchEvent): void => {
|
|
2369
|
+
if (this.hasAttribute("scroll-lock") || !this.scrollContainer) return;
|
|
2370
|
+
if (e.touches.length < 2) return;
|
|
2371
|
+
|
|
2372
|
+
if (this.scrollAnimationFrame) {
|
|
2373
|
+
this.cancelScrollAnimation();
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
const distance = this.getTouchDistance(e.touches);
|
|
2377
|
+
if (distance <= 0) return;
|
|
2378
|
+
|
|
2379
|
+
e.preventDefault();
|
|
2380
|
+
this.isPinchZooming = true;
|
|
2381
|
+
this.pinchStartDistance = distance;
|
|
2382
|
+
this.pinchStartHeight = this.dayHeight;
|
|
2383
|
+
this.zoomViewportY = this.getTouchCenterY(e.touches);
|
|
2384
|
+
this.pinchStartOriginY = this.zoomViewportY + this.scrollTop;
|
|
2385
|
+
this.zoomOriginY = this.pinchStartOriginY;
|
|
2386
|
+
|
|
2387
|
+
if (this.isFiltered && this.historyIndex === 0) {
|
|
2388
|
+
this.replaceFilterInHistory();
|
|
2389
|
+
} else {
|
|
2390
|
+
this.debouncedSaveToHistory();
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
onTouchMove = (e: TouchEvent): void => {
|
|
2395
|
+
if (!this.isPinchZooming || !this.scrollContainer) return;
|
|
2396
|
+
if (e.touches.length < 2 || this.pinchStartDistance <= 0) return;
|
|
2397
|
+
|
|
2398
|
+
e.preventDefault();
|
|
2399
|
+
const distance = this.getTouchDistance(e.touches);
|
|
2400
|
+
if (distance <= 0) return;
|
|
2401
|
+
|
|
2402
|
+
this.zoomViewportY = this.getTouchCenterY(e.touches);
|
|
2403
|
+
const scaleRatio = distance / this.pinchStartDistance;
|
|
2404
|
+
const newHeight = Math.max(
|
|
2405
|
+
MIN_DAY_HEIGHT,
|
|
2406
|
+
Math.min(MAX_DAY_HEIGHT, this.pinchStartHeight * scaleRatio),
|
|
2407
|
+
);
|
|
2408
|
+
const zoomRatio = newHeight / this.pinchStartHeight;
|
|
2409
|
+
const newOriginY = this.pinchStartOriginY * zoomRatio;
|
|
2410
|
+
const newScrollTop = newOriginY - this.zoomViewportY;
|
|
2411
|
+
|
|
2412
|
+
this.setView(newHeight, newScrollTop, false, false);
|
|
2413
|
+
this.zoomOriginY = newOriginY;
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
onTouchEnd = (e: TouchEvent): void => {
|
|
2417
|
+
if (e.touches.length >= 2) {
|
|
2418
|
+
this.onTouchStart(e);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
this.isPinchZooming = false;
|
|
2423
|
+
this.pinchStartDistance = 0;
|
|
2424
|
+
this.syncScrollDomToState();
|
|
1837
2425
|
};
|
|
1838
2426
|
|
|
1839
2427
|
lastPointerY = 0;
|
|
@@ -1970,7 +2558,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1970
2558
|
this.scrollContainer
|
|
1971
2559
|
) {
|
|
1972
2560
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
1973
|
-
const currentX = e.clientX
|
|
2561
|
+
const currentX = this.getContentXFromClientX(e.clientX);
|
|
1974
2562
|
const currentY = e.clientY - rect.top + this.scrollTop;
|
|
1975
2563
|
|
|
1976
2564
|
// Start selection
|
|
@@ -1992,7 +2580,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1992
2580
|
this.selection = {
|
|
1993
2581
|
startX: this.selectionStartX,
|
|
1994
2582
|
startY: this.selectionStartY,
|
|
1995
|
-
endX: e.clientX
|
|
2583
|
+
endX: this.getContentXFromClientX(e.clientX),
|
|
1996
2584
|
endY: e.clientY - rect.top + this.scrollTop,
|
|
1997
2585
|
};
|
|
1998
2586
|
this.requestUpdate();
|
|
@@ -2001,7 +2589,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2001
2589
|
// Event creation drag
|
|
2002
2590
|
if (this.eventCreationStart && this.scrollContainer) {
|
|
2003
2591
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2004
|
-
const currentX = e.clientX
|
|
2592
|
+
const currentX = this.getContentXFromClientX(e.clientX);
|
|
2005
2593
|
const currentY = e.clientY - rect.top + this.scrollTop;
|
|
2006
2594
|
this.isCreatingEvent = true;
|
|
2007
2595
|
|
|
@@ -2043,7 +2631,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
2043
2631
|
this.eventCreationEnd = { x: currentX, y: currentY };
|
|
2044
2632
|
this.eventCreationPreviousShiftPressed = e.shiftKey;
|
|
2045
2633
|
this.renderDateLabels();
|
|
2046
|
-
this.renderEventCreationPreview();
|
|
2047
2634
|
}
|
|
2048
2635
|
|
|
2049
2636
|
// Event move drag - now handled by HTML5 drag and drop
|
|
@@ -2055,7 +2642,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2055
2642
|
// Event resize drag
|
|
2056
2643
|
if (this.resizingEvent && this.resizingEdge && this.scrollContainer) {
|
|
2057
2644
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2058
|
-
const currentX = e.clientX
|
|
2645
|
+
const currentX = this.getContentXFromClientX(e.clientX);
|
|
2059
2646
|
const currentY = e.clientY - rect.top + this.scrollTop;
|
|
2060
2647
|
this.isResizingEvent = true;
|
|
2061
2648
|
|
|
@@ -2109,14 +2696,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2109
2696
|
}
|
|
2110
2697
|
if (this.isDraggingZoom) {
|
|
2111
2698
|
this.isDraggingZoom = false;
|
|
2112
|
-
|
|
2113
|
-
if (this.scrollContainer && this.scrollContent) {
|
|
2114
|
-
if (this.scrollContainer.scrollHeight < this._scrollTop) {
|
|
2115
|
-
this.scrollContent.style.minHeight =
|
|
2116
|
-
this._scrollTop + window.innerHeight + "px";
|
|
2117
|
-
}
|
|
2118
|
-
this.scrollContainer.scrollTop = this._scrollTop;
|
|
2119
|
-
}
|
|
2699
|
+
this.syncScrollDomToState();
|
|
2120
2700
|
}
|
|
2121
2701
|
if (this.isDraggingMinimap) {
|
|
2122
2702
|
this.isDraggingMinimap = false;
|
|
@@ -2188,7 +2768,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
2188
2768
|
const isZoomedOut = this.dayHeight < TIME_SCALE_DAY_HEIGHT;
|
|
2189
2769
|
this.dispatchEvent(
|
|
2190
2770
|
new CustomEvent("create-event", {
|
|
2191
|
-
detail: {
|
|
2771
|
+
detail: {
|
|
2772
|
+
start: snappedStart,
|
|
2773
|
+
end: snappedEnd,
|
|
2774
|
+
isAllDay: isZoomedOut,
|
|
2775
|
+
},
|
|
2192
2776
|
bubbles: true,
|
|
2193
2777
|
}),
|
|
2194
2778
|
);
|
|
@@ -2254,6 +2838,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
2254
2838
|
this.resizingOriginalStart = null;
|
|
2255
2839
|
this.resizingOriginalEnd = null;
|
|
2256
2840
|
this.isResizingEvent = false;
|
|
2841
|
+
if (this.scrollContainer) {
|
|
2842
|
+
this.scrollContainer.style.cursor = "";
|
|
2843
|
+
}
|
|
2257
2844
|
this.renderCanvas();
|
|
2258
2845
|
this.requestUpdate();
|
|
2259
2846
|
}
|
|
@@ -2273,6 +2860,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2273
2860
|
// Close event detail overlay
|
|
2274
2861
|
this.selectedEventForDetail = null;
|
|
2275
2862
|
this.selectedEventRect = null;
|
|
2863
|
+
this.clearDescriptionSummary();
|
|
2276
2864
|
|
|
2277
2865
|
if (hadSelection) {
|
|
2278
2866
|
this.dispatchEvent(
|
|
@@ -2323,6 +2911,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2323
2911
|
this.internal.clearSelection();
|
|
2324
2912
|
this.selectedEventForDetail = null;
|
|
2325
2913
|
this.selectedEventRect = null;
|
|
2914
|
+
this.clearDescriptionSummary();
|
|
2326
2915
|
this.dispatchEvent(
|
|
2327
2916
|
new CustomEvent("selection-change", {
|
|
2328
2917
|
detail: { selectedEvents: [] },
|
|
@@ -2355,6 +2944,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2355
2944
|
this.selectedEventForDetail = null;
|
|
2356
2945
|
this.selectedEventRect = null;
|
|
2357
2946
|
this.isDescriptionExpanded = false;
|
|
2947
|
+
this.clearDescriptionSummary();
|
|
2358
2948
|
this.renderCanvas();
|
|
2359
2949
|
this.requestUpdate();
|
|
2360
2950
|
this.dispatchEvent(
|
|
@@ -2370,22 +2960,26 @@ export class CalendarViewElement extends LitElement {
|
|
|
2370
2960
|
if (!this.scrollContainer || this.isDraggingZoom) return;
|
|
2371
2961
|
|
|
2372
2962
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2373
|
-
const
|
|
2963
|
+
const viewportX = e.clientX - rect.left;
|
|
2964
|
+
const contentX = this.getContentXFromClientX(e.clientX);
|
|
2374
2965
|
const y = e.clientY - rect.top + this.scrollTop;
|
|
2375
2966
|
|
|
2376
2967
|
// Update cursor position for status bar
|
|
2377
|
-
this.cursorPosition = { x, y };
|
|
2968
|
+
this.cursorPosition = { x: contentX, y };
|
|
2378
2969
|
|
|
2379
2970
|
// Check for resize handles first — suppressed when alt key is active
|
|
2380
|
-
const resizeHandle = !e.altKey ? this.getResizeHandle(
|
|
2971
|
+
const resizeHandle = !e.altKey ? this.getResizeHandle(viewportX, y) : null;
|
|
2381
2972
|
if (resizeHandle && !this.isResizingEvent && !this.isDraggingEvent) {
|
|
2382
|
-
this.scrollContainer.style.cursor =
|
|
2973
|
+
this.scrollContainer.style.cursor =
|
|
2974
|
+
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
2383
2975
|
} else if (!this.isResizingEvent && !this.isDraggingEvent) {
|
|
2384
2976
|
this.scrollContainer.style.cursor = "";
|
|
2385
2977
|
}
|
|
2386
2978
|
|
|
2387
2979
|
// Check for event hover — suppressed when alt key is active
|
|
2388
|
-
const hoveredEvent = !e.altKey
|
|
2980
|
+
const hoveredEvent = !e.altKey
|
|
2981
|
+
? this.getEventAtPosition(viewportX, y)
|
|
2982
|
+
: null;
|
|
2389
2983
|
const newHoveredId = hoveredEvent ? hoveredEvent.id : null;
|
|
2390
2984
|
|
|
2391
2985
|
if (newHoveredId !== this.hoveredEventId) {
|
|
@@ -2395,7 +2989,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2395
2989
|
|
|
2396
2990
|
this.requestUpdate();
|
|
2397
2991
|
|
|
2398
|
-
if (
|
|
2992
|
+
if (viewportX < LEFT_GUTTER_WIDTH) {
|
|
2399
2993
|
this.scrollContainer.classList.add("zoom-cursor");
|
|
2400
2994
|
} else {
|
|
2401
2995
|
this.scrollContainer.classList.remove("zoom-cursor");
|
|
@@ -2440,7 +3034,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2440
3034
|
// Update visual preview
|
|
2441
3035
|
if (this.scrollContainer) {
|
|
2442
3036
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2443
|
-
const x = e.clientX
|
|
3037
|
+
const x = this.getContentXFromClientX(e.clientX);
|
|
2444
3038
|
const y = e.clientY - rect.top + this.scrollTop;
|
|
2445
3039
|
this.movingEventEnd = { x, y };
|
|
2446
3040
|
this.renderCanvas(); // Re-render canvas with preview
|
|
@@ -2484,7 +3078,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2484
3078
|
) {
|
|
2485
3079
|
// Handle internal event drop
|
|
2486
3080
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2487
|
-
const dropX = e.clientX
|
|
3081
|
+
const dropX = this.getContentXFromClientX(e.clientX);
|
|
2488
3082
|
const dropY = e.clientY - rect.top + this.scrollTop;
|
|
2489
3083
|
|
|
2490
3084
|
const originDate = this.convertPositionToDateTime(
|
|
@@ -2518,11 +3112,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
2518
3112
|
} else {
|
|
2519
3113
|
// Move event
|
|
2520
3114
|
this.dispatchEvent(
|
|
2521
|
-
new CustomEvent("
|
|
3115
|
+
new CustomEvent("update-event", {
|
|
2522
3116
|
detail: {
|
|
2523
3117
|
event: this.movingEvent,
|
|
2524
|
-
start: newStart,
|
|
2525
|
-
end: newEnd,
|
|
3118
|
+
updates: { start: newStart, end: newEnd },
|
|
2526
3119
|
},
|
|
2527
3120
|
bubbles: true,
|
|
2528
3121
|
}),
|
|
@@ -2545,7 +3138,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
2545
3138
|
|
|
2546
3139
|
onKeyDown = (e: KeyboardEvent): void => {
|
|
2547
3140
|
const focused = e.composedPath()[0] as HTMLElement;
|
|
2548
|
-
if (
|
|
3141
|
+
if (
|
|
3142
|
+
focused?.tagName === "INPUT" ||
|
|
3143
|
+
focused?.tagName === "TEXTAREA" ||
|
|
3144
|
+
focused?.isContentEditable
|
|
3145
|
+
)
|
|
3146
|
+
return;
|
|
2549
3147
|
if (e.altKey && !this.altKeyActive) {
|
|
2550
3148
|
this.altKeyActive = true;
|
|
2551
3149
|
this.dispatchEvent(new CustomEvent("meta-key-change", { bubbles: true }));
|
|
@@ -2561,7 +3159,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
2561
3159
|
|
|
2562
3160
|
onKeyUp = (e: KeyboardEvent): void => {
|
|
2563
3161
|
const focused = e.composedPath()[0] as HTMLElement;
|
|
2564
|
-
if (
|
|
3162
|
+
if (
|
|
3163
|
+
focused?.tagName === "INPUT" ||
|
|
3164
|
+
focused?.tagName === "TEXTAREA" ||
|
|
3165
|
+
focused?.isContentEditable
|
|
3166
|
+
)
|
|
3167
|
+
return;
|
|
2565
3168
|
if (!e.altKey && this.altKeyActive) {
|
|
2566
3169
|
this.altKeyActive = false;
|
|
2567
3170
|
this.dispatchEvent(new CustomEvent("meta-key-change", { bubbles: true }));
|
|
@@ -2682,6 +3285,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2682
3285
|
y: number,
|
|
2683
3286
|
): { event: CalendarEvent; edge: "start" | "end" } | null {
|
|
2684
3287
|
const RESIZE_HANDLE_SIZE = 8; // pixels from edge to detect resize
|
|
3288
|
+
const isZoomedIn = this.dayHeight >= TIME_SCALE_DAY_HEIGHT;
|
|
2685
3289
|
|
|
2686
3290
|
// Check in reverse order (top to bottom rendering)
|
|
2687
3291
|
for (let i = this.eventRects.length - 1; i >= 0; i--) {
|
|
@@ -2691,18 +3295,33 @@ export class CalendarViewElement extends LitElement {
|
|
|
2691
3295
|
// Skip read-only events
|
|
2692
3296
|
if (rect.event.readOnly) continue;
|
|
2693
3297
|
|
|
2694
|
-
|
|
2695
|
-
|
|
3298
|
+
if (isZoomedIn) {
|
|
3299
|
+
// In time-scale view, resize vertically from the top/bottom edges.
|
|
3300
|
+
if (x < rect.x || x > rect.x + rect.width) continue;
|
|
3301
|
+
|
|
3302
|
+
if (y >= rect.y && y <= rect.y + RESIZE_HANDLE_SIZE) {
|
|
3303
|
+
return { event: rect.event, edge: "start" };
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
if (
|
|
3307
|
+
y >= rect.y + rect.height - RESIZE_HANDLE_SIZE &&
|
|
3308
|
+
y <= rect.y + rect.height
|
|
3309
|
+
) {
|
|
3310
|
+
return { event: rect.event, edge: "end" };
|
|
3311
|
+
}
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
2696
3314
|
|
|
2697
|
-
//
|
|
2698
|
-
if (y
|
|
3315
|
+
// In zoomed-out view, resize horizontally from the left/right edges.
|
|
3316
|
+
if (y < rect.y || y > rect.y + rect.height) continue;
|
|
3317
|
+
|
|
3318
|
+
if (x >= rect.x && x <= rect.x + RESIZE_HANDLE_SIZE) {
|
|
2699
3319
|
return { event: rect.event, edge: "start" };
|
|
2700
3320
|
}
|
|
2701
3321
|
|
|
2702
|
-
// Check bottom edge (resize end)
|
|
2703
3322
|
if (
|
|
2704
|
-
|
|
2705
|
-
|
|
3323
|
+
x >= rect.x + rect.width - RESIZE_HANDLE_SIZE &&
|
|
3324
|
+
x <= rect.x + rect.width
|
|
2706
3325
|
) {
|
|
2707
3326
|
return { event: rect.event, edge: "end" };
|
|
2708
3327
|
}
|
|
@@ -2725,29 +3344,34 @@ export class CalendarViewElement extends LitElement {
|
|
|
2725
3344
|
}
|
|
2726
3345
|
|
|
2727
3346
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2728
|
-
const
|
|
3347
|
+
const viewportX = e.clientX - rect.left;
|
|
3348
|
+
const x = this.getContentXFromClientX(e.clientX);
|
|
2729
3349
|
const y = e.clientY - rect.top + this.scrollTop;
|
|
2730
3350
|
|
|
2731
3351
|
// Check if clicking on zoom handle area (left gutter)
|
|
2732
|
-
if (
|
|
3352
|
+
if (viewportX < LEFT_GUTTER_WIDTH) {
|
|
2733
3353
|
this.onZoomHandleMouseDown(e);
|
|
2734
3354
|
return;
|
|
2735
3355
|
}
|
|
2736
3356
|
|
|
2737
3357
|
// Check if clicking on a resize handle first
|
|
2738
|
-
const resizeHandle = !e.altKey ? this.getResizeHandle(
|
|
3358
|
+
const resizeHandle = !e.altKey ? this.getResizeHandle(viewportX, y) : null;
|
|
2739
3359
|
if (resizeHandle) {
|
|
2740
3360
|
this.resizingEvent = resizeHandle.event;
|
|
2741
3361
|
this.resizingEdge = resizeHandle.edge;
|
|
2742
3362
|
this.resizingOriginalStart = new Date(resizeHandle.event.start);
|
|
2743
3363
|
this.resizingOriginalEnd = new Date(resizeHandle.event.end);
|
|
2744
3364
|
this.isResizingEvent = false; // Will be set to true on first mouse move
|
|
3365
|
+
this.scrollContainer.style.cursor =
|
|
3366
|
+
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
2745
3367
|
return;
|
|
2746
3368
|
}
|
|
2747
3369
|
|
|
2748
3370
|
// Check if clicking on an event — defer click vs drag to mouseup
|
|
2749
3371
|
// Skip event interaction if Alt/Option is held (create new event instead)
|
|
2750
|
-
const clickedEvent = !e.altKey
|
|
3372
|
+
const clickedEvent = !e.altKey
|
|
3373
|
+
? this.getEventAtPosition(viewportX, y)
|
|
3374
|
+
: null;
|
|
2751
3375
|
if (clickedEvent) {
|
|
2752
3376
|
// Clear selection when starting to drag/move an event
|
|
2753
3377
|
const hadSelection = this.internal.getSelectedEvents().length > 0;
|
|
@@ -2764,6 +3388,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2764
3388
|
// Close event detail overlay
|
|
2765
3389
|
this.selectedEventForDetail = null;
|
|
2766
3390
|
this.selectedEventRect = null;
|
|
3391
|
+
this.clearDescriptionSummary();
|
|
2767
3392
|
|
|
2768
3393
|
this.movingEvent = clickedEvent;
|
|
2769
3394
|
this.movingEventOrigin = { x, y };
|
|
@@ -2779,6 +3404,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2779
3404
|
// Close event detail overlay when clicking outside of events
|
|
2780
3405
|
this.selectedEventForDetail = null;
|
|
2781
3406
|
this.selectedEventRect = null;
|
|
3407
|
+
this.clearDescriptionSummary();
|
|
2782
3408
|
|
|
2783
3409
|
// Clear selection when clicking on empty space
|
|
2784
3410
|
const hadSelection = this.internal.getSelectedEvents().length > 0;
|
|
@@ -2837,22 +3463,20 @@ export class CalendarViewElement extends LitElement {
|
|
|
2837
3463
|
const minY = Math.min(this.selection.startY, this.selection.endY);
|
|
2838
3464
|
const maxY = Math.max(this.selection.startY, this.selection.endY);
|
|
2839
3465
|
|
|
2840
|
-
const
|
|
2841
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
2842
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
3466
|
+
const dayWidth = this.getDayWidth();
|
|
2843
3467
|
|
|
2844
3468
|
// Find column indices from X coordinates
|
|
2845
3469
|
const startCol = Math.max(
|
|
2846
3470
|
0,
|
|
2847
3471
|
Math.min(
|
|
2848
|
-
this.
|
|
3472
|
+
this.columnsPerRow - 1,
|
|
2849
3473
|
Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
2850
3474
|
),
|
|
2851
3475
|
);
|
|
2852
3476
|
const endCol = Math.max(
|
|
2853
3477
|
0,
|
|
2854
3478
|
Math.min(
|
|
2855
|
-
this.
|
|
3479
|
+
this.columnsPerRow - 1,
|
|
2856
3480
|
Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
2857
3481
|
),
|
|
2858
3482
|
);
|
|
@@ -2957,7 +3581,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
2957
3581
|
if (event.visualStyle === "heatmap") continue;
|
|
2958
3582
|
|
|
2959
3583
|
// Only select events from the active calendar
|
|
2960
|
-
if (
|
|
3584
|
+
if (
|
|
3585
|
+
this.activeCalendarId &&
|
|
3586
|
+
event.calendarId !== this.activeCalendarId &&
|
|
3587
|
+
event.sourceId !== this.activeCalendarId
|
|
3588
|
+
)
|
|
3589
|
+
continue;
|
|
2961
3590
|
|
|
2962
3591
|
const eventStartTime = event.start.getTime();
|
|
2963
3592
|
const eventEndTime = event.end.getTime();
|
|
@@ -2986,6 +3615,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2986
3615
|
// Close event detail overlay when replacing selection
|
|
2987
3616
|
this.selectedEventForDetail = null;
|
|
2988
3617
|
this.selectedEventRect = null;
|
|
3618
|
+
this.clearDescriptionSummary();
|
|
2989
3619
|
}
|
|
2990
3620
|
for (const event of selectedEvents) {
|
|
2991
3621
|
this.internal.selectEvent(event, "add");
|
|
@@ -3164,7 +3794,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3164
3794
|
|
|
3165
3795
|
onZoomSliderChange = (e: Event): void => {
|
|
3166
3796
|
const slider = e.target as HTMLInputElement;
|
|
3167
|
-
const newHeight = parseInt(slider.value, 10);
|
|
3797
|
+
const newHeight = Number.parseInt(slider.value, 10);
|
|
3168
3798
|
const oldHeight = this.dayHeight;
|
|
3169
3799
|
|
|
3170
3800
|
if (!this.scrollContainer) {
|
|
@@ -3184,6 +3814,38 @@ export class CalendarViewElement extends LitElement {
|
|
|
3184
3814
|
this.setView(newHeight, newScrollTop);
|
|
3185
3815
|
};
|
|
3186
3816
|
|
|
3817
|
+
onDayColumnWidthSliderChange = (e: Event): void => {
|
|
3818
|
+
const slider = e.target as HTMLInputElement;
|
|
3819
|
+
const newWidth = Number.parseInt(slider.value, 10);
|
|
3820
|
+
|
|
3821
|
+
if (!this.scrollContainer) {
|
|
3822
|
+
this.minDayColumnWidth = newWidth;
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
const oldGridWidth = this.getGridWidth();
|
|
3827
|
+
const viewportWidth = this.scrollContainer.clientWidth;
|
|
3828
|
+
const viewportCenterX = viewportWidth / 2;
|
|
3829
|
+
const contentCenterX = this.scrollLeft + viewportCenterX;
|
|
3830
|
+
|
|
3831
|
+
this.minDayColumnWidth = newWidth;
|
|
3832
|
+
|
|
3833
|
+
const newGridWidth = this.getGridWidth();
|
|
3834
|
+
const scaleRatio = newGridWidth / oldGridWidth;
|
|
3835
|
+
const newContentCenterX = contentCenterX * scaleRatio;
|
|
3836
|
+
const maxScrollLeft = Math.max(
|
|
3837
|
+
0,
|
|
3838
|
+
LEFT_GUTTER_WIDTH + newGridWidth - viewportWidth,
|
|
3839
|
+
);
|
|
3840
|
+
const newScrollLeft = Math.max(
|
|
3841
|
+
0,
|
|
3842
|
+
Math.min(maxScrollLeft, newContentCenterX - viewportCenterX),
|
|
3843
|
+
);
|
|
3844
|
+
|
|
3845
|
+
this.scrollContainer.scrollLeft = newScrollLeft;
|
|
3846
|
+
this.renderCanvas();
|
|
3847
|
+
};
|
|
3848
|
+
|
|
3187
3849
|
async onEventClick(event: CalendarEvent, e: MouseEvent): Promise<void> {
|
|
3188
3850
|
const isCmdOrCtrl = e.metaKey || e.ctrlKey;
|
|
3189
3851
|
|
|
@@ -3195,6 +3857,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3195
3857
|
|
|
3196
3858
|
// Show event detail overlay for single selection
|
|
3197
3859
|
if (!isCmdOrCtrl) {
|
|
3860
|
+
this.clearDescriptionSummary();
|
|
3198
3861
|
this.selectedEventForDetail = event;
|
|
3199
3862
|
this.isDescriptionExpanded = false;
|
|
3200
3863
|
// Find the event's rect and store it for positioning
|
|
@@ -3214,6 +3877,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3214
3877
|
this.selectedEventForDetail = null;
|
|
3215
3878
|
this.selectedEventRect = null;
|
|
3216
3879
|
this.isDescriptionExpanded = false;
|
|
3880
|
+
this.clearDescriptionSummary();
|
|
3217
3881
|
this.requestUpdate();
|
|
3218
3882
|
}
|
|
3219
3883
|
|
|
@@ -3275,6 +3939,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3275
3939
|
// Close event detail overlay when clearing selection
|
|
3276
3940
|
this.selectedEventForDetail = null;
|
|
3277
3941
|
this.selectedEventRect = null;
|
|
3942
|
+
this.clearDescriptionSummary();
|
|
3278
3943
|
|
|
3279
3944
|
if (hadSelection) {
|
|
3280
3945
|
this.dispatchEvent(
|
|
@@ -3328,296 +3993,135 @@ export class CalendarViewElement extends LitElement {
|
|
|
3328
3993
|
this.renderDateLabels();
|
|
3329
3994
|
}
|
|
3330
3995
|
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
return;
|
|
3334
|
-
|
|
3335
|
-
const originDate = this.convertPositionToDateTime(
|
|
3336
|
-
this.movingEventOrigin.x,
|
|
3337
|
-
this.movingEventOrigin.y,
|
|
3338
|
-
);
|
|
3339
|
-
const currentDate = this.convertPositionToDateTime(
|
|
3340
|
-
this.movingEventEnd.x,
|
|
3341
|
-
this.movingEventEnd.y,
|
|
3342
|
-
);
|
|
3343
|
-
if (!originDate || !currentDate) return;
|
|
3996
|
+
getPreviewEvents(): PreviewEventData[] {
|
|
3997
|
+
const previews: PreviewEventData[] = [];
|
|
3344
3998
|
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
};
|
|
3350
|
-
const newStart = snap15(
|
|
3351
|
-
new Date(this.movingEvent.start.getTime() + deltaMs),
|
|
3352
|
-
);
|
|
3353
|
-
const newEnd = snap15(new Date(this.movingEvent.end.getTime() + deltaMs));
|
|
3354
|
-
|
|
3355
|
-
// The grab point (originDate) relative to the event's midpoint tells us
|
|
3356
|
-
// which half the user grabbed — this is stable for the whole drag.
|
|
3357
|
-
const eventMidMs = (this.movingEvent.start.getTime() + this.movingEvent.end.getTime()) / 2;
|
|
3358
|
-
const useStartEdge = originDate.getTime() <= eventMidMs;
|
|
3359
|
-
|
|
3360
|
-
if (this.movingEventDuplicateMode) {
|
|
3361
|
-
const accent =
|
|
3362
|
-
getComputedStyle(this).getPropertyValue("--accent-primary").trim() ||
|
|
3363
|
-
"rgb(100, 150, 255)";
|
|
3364
|
-
this.renderVirtualEvent(
|
|
3365
|
-
newStart,
|
|
3366
|
-
newEnd,
|
|
3367
|
-
{
|
|
3368
|
-
fill: accent.replace("rgb", "rgba").replace(")", ", 0.3)"),
|
|
3369
|
-
stroke: accent.replace("rgb", "rgba").replace(")", ", 0.8)"),
|
|
3370
|
-
text: "white",
|
|
3371
|
-
dashed: false,
|
|
3372
|
-
},
|
|
3373
|
-
useStartEdge,
|
|
3374
|
-
);
|
|
3375
|
-
} else {
|
|
3376
|
-
const rgb = hexToRgb(this.movingEvent.color || "#888888");
|
|
3377
|
-
this.renderVirtualEvent(
|
|
3378
|
-
newStart,
|
|
3379
|
-
newEnd,
|
|
3380
|
-
{
|
|
3381
|
-
fill: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
3382
|
-
stroke: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
3383
|
-
text: "rgba(255, 255, 255, 0.5)",
|
|
3384
|
-
dashed: false,
|
|
3385
|
-
},
|
|
3386
|
-
useStartEdge,
|
|
3999
|
+
if (this.movingEvent && this.movingEventOrigin && this.movingEventEnd) {
|
|
4000
|
+
const originDate = this.convertPositionToDateTime(
|
|
4001
|
+
this.movingEventOrigin.x,
|
|
4002
|
+
this.movingEventOrigin.y,
|
|
3387
4003
|
);
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
|
|
3391
|
-
renderEventCreationPreview(): void {
|
|
3392
|
-
if (!this.eventCreationStart || !this.eventCreationEnd) return;
|
|
3393
|
-
|
|
3394
|
-
const startDate = this.convertPositionToDateTime(
|
|
3395
|
-
this.eventCreationStart.x,
|
|
3396
|
-
this.eventCreationStart.y,
|
|
3397
|
-
);
|
|
3398
|
-
const endDate = this.convertPositionToDateTime(
|
|
3399
|
-
this.eventCreationEnd.x,
|
|
3400
|
-
this.eventCreationEnd.y,
|
|
3401
|
-
);
|
|
3402
|
-
if (!startDate || !endDate) return;
|
|
3403
|
-
|
|
3404
|
-
let earlier: Date;
|
|
3405
|
-
let later: Date;
|
|
3406
|
-
|
|
3407
|
-
if (
|
|
3408
|
-
this.eventCreationShiftPressed &&
|
|
3409
|
-
this.eventCreationInitialDuration !== null
|
|
3410
|
-
) {
|
|
3411
|
-
// Shift pressed: move event with fixed duration to current mouse position
|
|
3412
4004
|
const currentDate = this.convertPositionToDateTime(
|
|
3413
|
-
this.
|
|
3414
|
-
this.
|
|
3415
|
-
);
|
|
3416
|
-
if (!currentDate) return;
|
|
3417
|
-
|
|
3418
|
-
// Position the event so it ends at the current mouse position
|
|
3419
|
-
later = new Date(currentDate);
|
|
3420
|
-
earlier = new Date(
|
|
3421
|
-
currentDate.getTime() - this.eventCreationInitialDuration,
|
|
4005
|
+
this.movingEventEnd.x,
|
|
4006
|
+
this.movingEventEnd.y,
|
|
3422
4007
|
);
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
4008
|
+
if (originDate && currentDate) {
|
|
4009
|
+
const deltaMs = currentDate.getTime() - originDate.getTime();
|
|
4010
|
+
const snap15 = (d: Date) => {
|
|
4011
|
+
d.setMinutes(Math.round(d.getMinutes() / 15) * 15, 0, 0);
|
|
4012
|
+
return d;
|
|
4013
|
+
};
|
|
4014
|
+
const start = snap15(
|
|
4015
|
+
new Date(this.movingEvent.start.getTime() + deltaMs),
|
|
4016
|
+
);
|
|
4017
|
+
const end = snap15(new Date(this.movingEvent.end.getTime() + deltaMs));
|
|
4018
|
+
if (this.dayHeight < TIME_SCALE_EVENT_THRESHOLD) {
|
|
4019
|
+
start.setHours(0, 0, 0, 0);
|
|
4020
|
+
end.setHours(23, 59, 59, 999);
|
|
4021
|
+
}
|
|
3428
4022
|
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
4023
|
+
if (this.movingEventDuplicateMode) {
|
|
4024
|
+
const accent =
|
|
4025
|
+
getComputedStyle(this)
|
|
4026
|
+
.getPropertyValue("--accent-primary")
|
|
4027
|
+
.trim() || "rgb(100, 150, 255)";
|
|
4028
|
+
previews.push({
|
|
4029
|
+
id: `move-preview-${this.movingEvent.id}`,
|
|
4030
|
+
start,
|
|
4031
|
+
end,
|
|
4032
|
+
color: {
|
|
4033
|
+
fill: accent.replace("rgb", "rgba").replace(")", ", 0.3)"),
|
|
4034
|
+
stroke: accent.replace("rgb", "rgba").replace(")", ", 0.8)"),
|
|
4035
|
+
text: "white",
|
|
4036
|
+
dashed: false,
|
|
4037
|
+
},
|
|
4038
|
+
});
|
|
4039
|
+
} else {
|
|
4040
|
+
const rgb = hexToRgb(this.movingEvent.color || "#888888");
|
|
4041
|
+
previews.push({
|
|
4042
|
+
id: `move-preview-${this.movingEvent.id}`,
|
|
4043
|
+
start,
|
|
4044
|
+
end,
|
|
4045
|
+
excludeEventId: this.movingEvent.id,
|
|
4046
|
+
color: {
|
|
4047
|
+
fill: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
4048
|
+
stroke: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
4049
|
+
text: "rgba(255, 255, 255, 0.5)",
|
|
4050
|
+
dashed: false,
|
|
4051
|
+
},
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
3435
4055
|
}
|
|
3436
4056
|
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
const rgb = hexToRgb(this.activeCalendarColor);
|
|
3442
|
-
fill = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.3)`;
|
|
3443
|
-
stroke = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.8)`;
|
|
3444
|
-
} else {
|
|
3445
|
-
const accent =
|
|
3446
|
-
getComputedStyle(this).getPropertyValue("--accent-primary").trim() ||
|
|
3447
|
-
"rgb(100, 150, 255)";
|
|
3448
|
-
fill = accent.replace("rgb", "rgba").replace(")", ", 0.3)");
|
|
3449
|
-
stroke = accent.replace("rgb", "rgba").replace(")", ", 0.8)");
|
|
3450
|
-
}
|
|
3451
|
-
// In shift mode the cursor tracks the end; otherwise it tracks whichever
|
|
3452
|
-
// edge is away from the fixed anchor (eventCreationStart).
|
|
3453
|
-
const useStartEdge = this.eventCreationShiftPressed
|
|
3454
|
-
? false
|
|
3455
|
-
: this.eventCreationEnd.y <= this.eventCreationStart.y;
|
|
3456
|
-
|
|
3457
|
-
this.renderVirtualEvent(earlier, later, { fill, stroke, text: "white" }, useStartEdge);
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
renderVirtualEvent(
|
|
3461
|
-
start: Date,
|
|
3462
|
-
end: Date,
|
|
3463
|
-
color: { fill: string; stroke: string; text: string; dashed?: boolean },
|
|
3464
|
-
useStartEdge = true,
|
|
3465
|
-
): void {
|
|
3466
|
-
if (!this.overlayCanvas || !this.overlayCtx || !this.scrollContainer)
|
|
3467
|
-
return;
|
|
3468
|
-
|
|
3469
|
-
const ctx = this.overlayCtx;
|
|
3470
|
-
const fontFamily = getComputedStyle(this).fontFamily;
|
|
3471
|
-
const scrollRect = this.scrollContainer.getBoundingClientRect();
|
|
3472
|
-
const gridWidth = scrollRect.width - LEFT_GUTTER_WIDTH - MINIMAP_WIDTH;
|
|
3473
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
3474
|
-
|
|
3475
|
-
const getTimeY = (day: Date, hours: number, minutes: number) => {
|
|
3476
|
-
const week = this.weeks.find((w) =>
|
|
3477
|
-
w.days.some((d) => d.toDateString() === day.toDateString()),
|
|
3478
|
-
);
|
|
3479
|
-
if (!week) return null;
|
|
3480
|
-
const dayIndex = week.days.findIndex(
|
|
3481
|
-
(d) => d.toDateString() === day.toDateString(),
|
|
3482
|
-
);
|
|
3483
|
-
if (dayIndex < 0) return null;
|
|
3484
|
-
const { row } = this.getDayVisualPosition(dayIndex);
|
|
3485
|
-
const totalMinutes = hours * 60 + minutes;
|
|
3486
|
-
const rowY = week.yOffset + row * this.dayHeight;
|
|
3487
|
-
return rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop;
|
|
3488
|
-
};
|
|
3489
|
-
|
|
3490
|
-
const getDayColumnX = (day: Date) => {
|
|
3491
|
-
const week = this.weeks.find((w) =>
|
|
3492
|
-
w.days.some((d) => d.toDateString() === day.toDateString()),
|
|
4057
|
+
if (this.eventCreationStart && this.eventCreationEnd) {
|
|
4058
|
+
const startDate = this.convertPositionToDateTime(
|
|
4059
|
+
this.eventCreationStart.x,
|
|
4060
|
+
this.eventCreationStart.y,
|
|
3493
4061
|
);
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
4062
|
+
const endDate = this.convertPositionToDateTime(
|
|
4063
|
+
this.eventCreationEnd.x,
|
|
4064
|
+
this.eventCreationEnd.y,
|
|
3497
4065
|
);
|
|
3498
|
-
if (
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
ctx.setLineDash([]);
|
|
3516
|
-
};
|
|
3517
|
-
|
|
3518
|
-
const sameDay = start.toDateString() === end.toDateString();
|
|
3519
|
-
|
|
3520
|
-
if (sameDay) {
|
|
3521
|
-
const colX = getDayColumnX(start);
|
|
3522
|
-
const top = getTimeY(start, start.getHours(), start.getMinutes());
|
|
3523
|
-
const bottom = getTimeY(end, end.getHours(), end.getMinutes());
|
|
3524
|
-
if (colX != null && top != null && bottom != null)
|
|
3525
|
-
drawBlock(colX, top, bottom);
|
|
3526
|
-
} else {
|
|
3527
|
-
const current = new Date(start);
|
|
3528
|
-
current.setHours(0, 0, 0, 0);
|
|
3529
|
-
const endDay = new Date(end);
|
|
3530
|
-
endDay.setHours(23, 59, 59, 999);
|
|
3531
|
-
while (current <= endDay) {
|
|
3532
|
-
const colX = getDayColumnX(current);
|
|
3533
|
-
if (colX != null) {
|
|
3534
|
-
const isFirst = current.toDateString() === start.toDateString();
|
|
3535
|
-
const isLast = current.toDateString() === end.toDateString();
|
|
3536
|
-
let top: number | null;
|
|
3537
|
-
let bottom: number | null;
|
|
3538
|
-
if (isFirst) {
|
|
3539
|
-
top = getTimeY(current, start.getHours(), start.getMinutes());
|
|
3540
|
-
bottom = getTimeY(current, 23, 59);
|
|
3541
|
-
} else if (isLast) {
|
|
3542
|
-
top = getTimeY(current, 0, 0);
|
|
3543
|
-
bottom = getTimeY(current, end.getHours(), end.getMinutes());
|
|
4066
|
+
if (startDate && endDate) {
|
|
4067
|
+
let earlier: Date;
|
|
4068
|
+
let later: Date;
|
|
4069
|
+
|
|
4070
|
+
if (
|
|
4071
|
+
this.eventCreationShiftPressed &&
|
|
4072
|
+
this.eventCreationInitialDuration !== null
|
|
4073
|
+
) {
|
|
4074
|
+
const currentDate = this.convertPositionToDateTime(
|
|
4075
|
+
this.eventCreationEnd.x,
|
|
4076
|
+
this.eventCreationEnd.y,
|
|
4077
|
+
);
|
|
4078
|
+
if (currentDate) {
|
|
4079
|
+
later = new Date(currentDate);
|
|
4080
|
+
earlier = new Date(
|
|
4081
|
+
currentDate.getTime() - this.eventCreationInitialDuration,
|
|
4082
|
+
);
|
|
3544
4083
|
} else {
|
|
3545
|
-
|
|
3546
|
-
|
|
4084
|
+
earlier = startDate < endDate ? startDate : endDate;
|
|
4085
|
+
later = startDate < endDate ? endDate : startDate;
|
|
3547
4086
|
}
|
|
3548
|
-
|
|
4087
|
+
} else {
|
|
4088
|
+
earlier = startDate < endDate ? startDate : endDate;
|
|
4089
|
+
later = startDate < endDate ? endDate : startDate;
|
|
3549
4090
|
}
|
|
3550
|
-
current.setDate(current.getDate() + 1);
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
// Time label
|
|
3555
|
-
const fmtTime = (d: Date) =>
|
|
3556
|
-
`${d.getHours().toString().padStart(2, "0")}:${d
|
|
3557
|
-
.getMinutes()
|
|
3558
|
-
.toString()
|
|
3559
|
-
.padStart(2, "0")}`;
|
|
3560
|
-
const fmtDate = (d: Date) => `${d.getDate()}.${d.getMonth() + 1}`;
|
|
3561
|
-
const label = sameDay
|
|
3562
|
-
? `${fmtTime(start)} – ${fmtTime(end)}`
|
|
3563
|
-
: `${fmtDate(start)} ${fmtTime(start)} – ${fmtDate(end)} ${fmtTime(end)}`;
|
|
3564
|
-
|
|
3565
|
-
const durationMs = Math.abs(end.getTime() - start.getTime());
|
|
3566
|
-
const durationMinutes = durationMs / (1000 * 60);
|
|
3567
|
-
const isShortEvent = durationMinutes < 15;
|
|
3568
4091
|
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
lastColX != null &&
|
|
3577
|
-
startY != null &&
|
|
3578
|
-
endY != null
|
|
3579
|
-
) {
|
|
3580
|
-
ctx.font = `600 11px ${fontFamily}`;
|
|
3581
|
-
ctx.fillStyle = color.text;
|
|
3582
|
-
ctx.textAlign = "left";
|
|
4092
|
+
if (this.dayHeight < TIME_SCALE_EVENT_THRESHOLD) {
|
|
4093
|
+
earlier.setHours(0, 0, 0, 0);
|
|
4094
|
+
later.setHours(23, 59, 59, 999);
|
|
4095
|
+
} else {
|
|
4096
|
+
earlier.setMinutes(Math.round(earlier.getMinutes() / 15) * 15, 0, 0);
|
|
4097
|
+
later.setMinutes(Math.round(later.getMinutes() / 15) * 15, 0, 0);
|
|
4098
|
+
}
|
|
3583
4099
|
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
"rgba(0, 0, 0, 0.8)";
|
|
3599
|
-
ctx.fillStyle = bgElevated;
|
|
3600
|
-
ctx.beginPath();
|
|
3601
|
-
ctx.roundRect(
|
|
3602
|
-
labelColX + 4,
|
|
3603
|
-
labelBgY,
|
|
3604
|
-
textWidth + bgPaddingX * 2,
|
|
3605
|
-
16,
|
|
3606
|
-
4,
|
|
3607
|
-
);
|
|
3608
|
-
ctx.fill();
|
|
4100
|
+
let fill: string;
|
|
4101
|
+
let stroke: string;
|
|
4102
|
+
if (this.activeCalendarColor) {
|
|
4103
|
+
const rgb = hexToRgb(this.activeCalendarColor);
|
|
4104
|
+
fill = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.3)`;
|
|
4105
|
+
stroke = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.8)`;
|
|
4106
|
+
} else {
|
|
4107
|
+
const accent =
|
|
4108
|
+
getComputedStyle(this)
|
|
4109
|
+
.getPropertyValue("--accent-primary")
|
|
4110
|
+
.trim() || "rgb(100, 150, 255)";
|
|
4111
|
+
fill = accent.replace("rgb", "rgba").replace(")", ", 0.3)");
|
|
4112
|
+
stroke = accent.replace("rgb", "rgba").replace(")", ", 0.8)");
|
|
4113
|
+
}
|
|
3609
4114
|
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
ctx.textBaseline = "top";
|
|
3617
|
-
const labelY = useStartEdge ? startY + 4 : endY - 18;
|
|
3618
|
-
ctx.fillText(label, labelColX + 8, labelY);
|
|
4115
|
+
previews.push({
|
|
4116
|
+
id: "creation-preview",
|
|
4117
|
+
start: earlier,
|
|
4118
|
+
end: later,
|
|
4119
|
+
color: { fill, stroke, text: "white", dashed: true },
|
|
4120
|
+
});
|
|
3619
4121
|
}
|
|
3620
4122
|
}
|
|
4123
|
+
|
|
4124
|
+
return previews;
|
|
3621
4125
|
}
|
|
3622
4126
|
|
|
3623
4127
|
renderDateLabels(): void {
|
|
@@ -3630,11 +4134,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
3630
4134
|
|
|
3631
4135
|
ctx.clearRect(0, 0, width, height);
|
|
3632
4136
|
|
|
3633
|
-
const dayWidth =
|
|
4137
|
+
const dayWidth = this.getDayWidth();
|
|
3634
4138
|
const scrollTop = this.scrollTop;
|
|
4139
|
+
const scrollLeft = this.scrollLeft;
|
|
3635
4140
|
const fontFamily = getComputedStyle(this).fontFamily;
|
|
3636
4141
|
|
|
3637
|
-
ctx.font = `600
|
|
4142
|
+
ctx.font = `600 12px ${fontFamily}`;
|
|
3638
4143
|
const textSecondary =
|
|
3639
4144
|
getComputedStyle(this).getPropertyValue("--text-secondary").trim() ||
|
|
3640
4145
|
"rgba(255, 255, 255, 0.6)";
|
|
@@ -3652,7 +4157,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3652
4157
|
if (!day) continue;
|
|
3653
4158
|
|
|
3654
4159
|
const { row, col } = this.getDayVisualPosition(dayIndex);
|
|
3655
|
-
const x = col * dayWidth;
|
|
4160
|
+
const x = col * dayWidth - scrollLeft;
|
|
3656
4161
|
const dayTop = week.yOffset + row * this.dayHeight - scrollTop;
|
|
3657
4162
|
const dayBottom = dayTop + this.dayHeight;
|
|
3658
4163
|
|
|
@@ -3691,6 +4196,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
3691
4196
|
}
|
|
3692
4197
|
}
|
|
3693
4198
|
}
|
|
4199
|
+
|
|
4200
|
+
this.renderMonthLabels(ctx, width, height, fontFamily);
|
|
3694
4201
|
}
|
|
3695
4202
|
|
|
3696
4203
|
renderWeekdayLabels(
|
|
@@ -3711,57 +4218,41 @@ export class CalendarViewElement extends LitElement {
|
|
|
3711
4218
|
getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
|
|
3712
4219
|
"rgba(30, 30, 30, 0.9)";
|
|
3713
4220
|
|
|
3714
|
-
ctx.font = `
|
|
4221
|
+
ctx.font = `600 12px ${fontFamily}`;
|
|
3715
4222
|
ctx.textAlign = "center";
|
|
3716
4223
|
ctx.textBaseline = "top";
|
|
3717
4224
|
|
|
3718
4225
|
const labelHeight = 16;
|
|
3719
|
-
const labelY =
|
|
3720
|
-
|
|
3721
|
-
// Find the first visible visual row
|
|
3722
|
-
const firstWeek = visibleWeeks[0];
|
|
3723
|
-
if (!firstWeek) return;
|
|
3724
|
-
|
|
3725
|
-
// Determine which visual rows are visible
|
|
3726
|
-
for (let row = 0; row < this.rowsPerWeek; row++) {
|
|
3727
|
-
const rowTop = firstWeek.yOffset + row * this.dayHeight - scrollTop;
|
|
3728
|
-
const rowBottom = rowTop + this.dayHeight;
|
|
3729
|
-
|
|
3730
|
-
// Check if this visual row is visible
|
|
3731
|
-
if (rowBottom < 0 || rowTop > height) continue;
|
|
4226
|
+
const labelY = 48; // Below sticky month label
|
|
3732
4227
|
|
|
3733
|
-
|
|
3734
|
-
const stickyY = Math.min(labelY, rowBottom - labelHeight - 2);
|
|
3735
|
-
if (stickyY < 0) continue;
|
|
4228
|
+
const stickyY = labelY;
|
|
3736
4229
|
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
if (dayIndex >= 7) continue;
|
|
3741
|
-
const dayName = weekdayNames[dayIndex];
|
|
3742
|
-
if (!dayName) continue;
|
|
4230
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
4231
|
+
const dayName = weekdayNames[dayIndex];
|
|
4232
|
+
if (!dayName) continue;
|
|
3743
4233
|
|
|
3744
|
-
|
|
4234
|
+
const x =
|
|
4235
|
+
LEFT_GUTTER_WIDTH +
|
|
4236
|
+
dayIndex * dayWidth +
|
|
4237
|
+
dayWidth / 2 -
|
|
4238
|
+
this.scrollLeft;
|
|
3745
4239
|
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
ctx.fill();
|
|
4240
|
+
const textWidth = ctx.measureText(dayName).width;
|
|
4241
|
+
const bgPaddingX = 6;
|
|
4242
|
+
const bgPaddingY = 2;
|
|
4243
|
+
ctx.fillStyle = bgPrimary;
|
|
4244
|
+
ctx.beginPath();
|
|
4245
|
+
ctx.roundRect(
|
|
4246
|
+
x - textWidth / 2 - bgPaddingX,
|
|
4247
|
+
stickyY,
|
|
4248
|
+
textWidth + bgPaddingX * 2,
|
|
4249
|
+
labelHeight,
|
|
4250
|
+
4,
|
|
4251
|
+
);
|
|
4252
|
+
ctx.fill();
|
|
3760
4253
|
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
|
|
3764
|
-
}
|
|
4254
|
+
ctx.fillStyle = textMuted;
|
|
4255
|
+
ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
|
|
3765
4256
|
}
|
|
3766
4257
|
}
|
|
3767
4258
|
|
|
@@ -3959,9 +4450,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3959
4450
|
): Date | null {
|
|
3960
4451
|
if (!this.scrollContainer) return null;
|
|
3961
4452
|
|
|
3962
|
-
const
|
|
3963
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
3964
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4453
|
+
const dayWidth = this.getDayWidth();
|
|
3965
4454
|
|
|
3966
4455
|
// Check if X is in the calendar grid area
|
|
3967
4456
|
if (x < LEFT_GUTTER_WIDTH) return null;
|
|
@@ -3969,7 +4458,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3969
4458
|
const col = Math.max(
|
|
3970
4459
|
0,
|
|
3971
4460
|
Math.min(
|
|
3972
|
-
this.
|
|
4461
|
+
this.columnsPerRow - 1,
|
|
3973
4462
|
Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
3974
4463
|
),
|
|
3975
4464
|
);
|
|
@@ -3983,7 +4472,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3983
4472
|
|
|
3984
4473
|
// Calculate which visual row within the week
|
|
3985
4474
|
const rowInWeek = Math.floor((y - week.yOffset) / this.dayHeight);
|
|
3986
|
-
const dayIndex = rowInWeek * this.
|
|
4475
|
+
const dayIndex = rowInWeek * this.columnsPerRow + col;
|
|
3987
4476
|
|
|
3988
4477
|
if (dayIndex < 0 || dayIndex > 6) return null;
|
|
3989
4478
|
|
|
@@ -4013,9 +4502,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4013
4502
|
convertDateTimeToPosition(date: Date): { x: number; y: number } | null {
|
|
4014
4503
|
if (!this.scrollContainer) return null;
|
|
4015
4504
|
|
|
4016
|
-
const
|
|
4017
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
4018
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4505
|
+
const dayWidth = this.getDayWidth();
|
|
4019
4506
|
|
|
4020
4507
|
// Find the week that contains this date
|
|
4021
4508
|
const dateStr = date.toDateString();
|
|
@@ -4122,6 +4609,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4122
4609
|
|
|
4123
4610
|
this.selectedEventForDetail = null;
|
|
4124
4611
|
this.selectedEventRect = null;
|
|
4612
|
+
this.clearDescriptionSummary();
|
|
4125
4613
|
this.requestUpdate();
|
|
4126
4614
|
}
|
|
4127
4615
|
|
|
@@ -4261,6 +4749,34 @@ export class CalendarViewElement extends LitElement {
|
|
|
4261
4749
|
}
|
|
4262
4750
|
|
|
4263
4751
|
const event = this.selectedEventForDetail;
|
|
4752
|
+
const eventDescription = sanitizeEventDescription(event.description ?? "");
|
|
4753
|
+
const eventDescriptionText = eventDescription.replaceAll("\\n", "\n");
|
|
4754
|
+
const useDescriptionSummary = eventDescription.trim().length > 200;
|
|
4755
|
+
const currentSummaryKey = this.getDescriptionSummaryTargetKey(event);
|
|
4756
|
+
const hasRequestedSummaryForCurrentEvent =
|
|
4757
|
+
this.requestedDescriptionSummaryKey === currentSummaryKey;
|
|
4758
|
+
const isSummaryForCurrentEvent =
|
|
4759
|
+
this.descriptionSummaryTargetKey === currentSummaryKey;
|
|
4760
|
+
const hasSummaryStateForCurrentEvent =
|
|
4761
|
+
hasRequestedSummaryForCurrentEvent || isSummaryForCurrentEvent;
|
|
4762
|
+
const streamedSummaryText = hasSummaryStateForCurrentEvent
|
|
4763
|
+
? this.descriptionSummaryText
|
|
4764
|
+
: "";
|
|
4765
|
+
const isSummaryLoading =
|
|
4766
|
+
useDescriptionSummary &&
|
|
4767
|
+
hasSummaryStateForCurrentEvent &&
|
|
4768
|
+
this.descriptionSummaryLoading;
|
|
4769
|
+
const shouldRenderSummary =
|
|
4770
|
+
useDescriptionSummary && hasSummaryStateForCurrentEvent;
|
|
4771
|
+
const descriptionToRender = shouldRenderSummary
|
|
4772
|
+
? streamedSummaryText ||
|
|
4773
|
+
(isSummaryLoading ? "Generating summary..." : eventDescriptionText)
|
|
4774
|
+
: eventDescriptionText;
|
|
4775
|
+
const summarizeButtonLabel = isSummaryLoading
|
|
4776
|
+
? "Summarizing..."
|
|
4777
|
+
: shouldRenderSummary
|
|
4778
|
+
? "Summarize again"
|
|
4779
|
+
: "Summarize";
|
|
4264
4780
|
|
|
4265
4781
|
const formatDate = (date: Date) => {
|
|
4266
4782
|
return new Intl.DateTimeFormat(this.locale, {
|
|
@@ -4399,6 +4915,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4399
4915
|
this.selectedEventForDetail = null;
|
|
4400
4916
|
this.selectedEventRect = null;
|
|
4401
4917
|
this.isDescriptionExpanded = false;
|
|
4918
|
+
this.clearDescriptionSummary();
|
|
4402
4919
|
this.requestUpdate();
|
|
4403
4920
|
}}
|
|
4404
4921
|
>×</button>
|
|
@@ -4406,7 +4923,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
4406
4923
|
|
|
4407
4924
|
<div class="event-detail-body">
|
|
4408
4925
|
${(() => {
|
|
4409
|
-
const teamsMatch =
|
|
4926
|
+
const teamsMatch = eventDescription?.match(
|
|
4927
|
+
/https:\/\/teams\.microsoft\.com\/[^\s<>"]+/,
|
|
4928
|
+
);
|
|
4410
4929
|
const teamsUrl = teamsMatch ? teamsMatch[0] : null;
|
|
4411
4930
|
if (!event.location && !teamsUrl) return null;
|
|
4412
4931
|
return html`
|
|
@@ -4414,7 +4933,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
4414
4933
|
<div class="event-detail-label">Location</div>
|
|
4415
4934
|
<div class="event-detail-value">
|
|
4416
4935
|
${event.location ? html`<div>${event.location}</div>` : null}
|
|
4417
|
-
${
|
|
4936
|
+
${
|
|
4937
|
+
teamsUrl
|
|
4938
|
+
? html`<a href="${teamsUrl}" class="event-detail-link" target="_blank" rel="noopener">Join Microsoft Teams Meeting</a>`
|
|
4939
|
+
: null
|
|
4940
|
+
}
|
|
4418
4941
|
</div>
|
|
4419
4942
|
</div>
|
|
4420
4943
|
`;
|
|
@@ -4509,24 +5032,64 @@ export class CalendarViewElement extends LitElement {
|
|
|
4509
5032
|
}
|
|
4510
5033
|
|
|
4511
5034
|
${
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
5035
|
+
useDescriptionSummary
|
|
5036
|
+
? html`
|
|
5037
|
+
<div class="event-detail-section">
|
|
5038
|
+
<div class="event-detail-label">Description</div>
|
|
5039
|
+
<div class="event-detail-description ${
|
|
5040
|
+
shouldRenderSummary || this.isDescriptionExpanded
|
|
5041
|
+
? "expanded"
|
|
5042
|
+
: ""
|
|
5043
|
+
}">
|
|
5044
|
+
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
5045
|
+
</div>
|
|
5046
|
+
${
|
|
5047
|
+
shouldRenderSummary && this.descriptionSummaryError
|
|
5048
|
+
? html`<div class="event-detail-value" style="opacity: 0.7;">Summary unavailable: ${this.descriptionSummaryError}. Showing original description.</div>`
|
|
5049
|
+
: null
|
|
5050
|
+
}
|
|
5051
|
+
<div class="description-actions">
|
|
5052
|
+
${
|
|
5053
|
+
!shouldRenderSummary
|
|
5054
|
+
? html`<button
|
|
5055
|
+
class="description-see-more"
|
|
5056
|
+
@click=${() => {
|
|
5057
|
+
this.isDescriptionExpanded =
|
|
5058
|
+
!this.isDescriptionExpanded;
|
|
5059
|
+
this.requestUpdate();
|
|
5060
|
+
}}
|
|
5061
|
+
>
|
|
5062
|
+
${
|
|
5063
|
+
this.isDescriptionExpanded ? "Show less" : "Show more"
|
|
5064
|
+
}
|
|
5065
|
+
</button>`
|
|
5066
|
+
: null
|
|
5067
|
+
}
|
|
5068
|
+
<button
|
|
5069
|
+
class="description-see-more"
|
|
5070
|
+
?disabled=${isSummaryLoading}
|
|
5071
|
+
@click=${() => this.requestDescriptionSummary(event)}
|
|
5072
|
+
>
|
|
5073
|
+
${summarizeButtonLabel}
|
|
5074
|
+
</button>
|
|
5075
|
+
</div>
|
|
5076
|
+
</div>
|
|
5077
|
+
`
|
|
5078
|
+
: event.readOnly ||
|
|
5079
|
+
(event.organizer != null &&
|
|
5080
|
+
!this.currentUserEmails.has(event.organizer.email))
|
|
5081
|
+
? eventDescription
|
|
5082
|
+
? html`
|
|
4517
5083
|
<div class="event-detail-section">
|
|
4518
5084
|
<div class="event-detail-label">Description</div>
|
|
4519
5085
|
<div class="event-detail-description ${
|
|
4520
5086
|
this.isDescriptionExpanded ? "expanded" : ""
|
|
4521
5087
|
}">
|
|
4522
|
-
<pre class="event-detail-value">${
|
|
4523
|
-
"\\n",
|
|
4524
|
-
"\n",
|
|
4525
|
-
)}</pre>
|
|
5088
|
+
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
4526
5089
|
</div>
|
|
4527
5090
|
${
|
|
4528
|
-
|
|
4529
|
-
|
|
5091
|
+
eventDescription.length > 300 ||
|
|
5092
|
+
eventDescription.split("\n").length > 8
|
|
4530
5093
|
? html`
|
|
4531
5094
|
<button
|
|
4532
5095
|
class="description-see-more"
|
|
@@ -4543,13 +5106,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
4543
5106
|
}
|
|
4544
5107
|
</div>
|
|
4545
5108
|
`
|
|
4546
|
-
|
|
4547
|
-
|
|
5109
|
+
: null
|
|
5110
|
+
: html`
|
|
4548
5111
|
<div class="event-detail-section">
|
|
4549
5112
|
<div class="event-detail-label">Description</div>
|
|
4550
5113
|
<textarea
|
|
4551
5114
|
class="event-detail-description-input"
|
|
4552
|
-
.value=${
|
|
5115
|
+
.value=${eventDescription}
|
|
4553
5116
|
placeholder="Add description..."
|
|
4554
5117
|
rows="3"
|
|
4555
5118
|
@input=${(e: Event) => {
|
|
@@ -4579,7 +5142,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4579
5142
|
clearTimeout(this.updateEventTimeout);
|
|
4580
5143
|
this.updateEventTimeout = null;
|
|
4581
5144
|
}
|
|
4582
|
-
if (newDescription !==
|
|
5145
|
+
if (newDescription !== eventDescription) {
|
|
4583
5146
|
this.dispatchEvent(
|
|
4584
5147
|
new CustomEvent("update-event", {
|
|
4585
5148
|
detail: {
|
|
@@ -4668,7 +5231,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
4668
5231
|
@mousedown=${this.onCanvasMouseDown}
|
|
4669
5232
|
@mousemove=${this.onScrollContainerMouseMove}
|
|
4670
5233
|
>
|
|
4671
|
-
<div class="scroll-content" style="height: ${
|
|
5234
|
+
<div class="scroll-content" style="height: ${
|
|
5235
|
+
this.totalHeight
|
|
5236
|
+
}px; width: ${this.getContentWidth()}px;">
|
|
4672
5237
|
</div>
|
|
4673
5238
|
${this.renderSelection()}
|
|
4674
5239
|
</div>
|
|
@@ -4682,27 +5247,42 @@ export class CalendarViewElement extends LitElement {
|
|
|
4682
5247
|
<div class="toolbar">
|
|
4683
5248
|
<div class="toolbar-left">
|
|
4684
5249
|
<button class="toolbar-button" title="Month" @click=${
|
|
4685
|
-
|
|
4686
|
-
|
|
5250
|
+
this.scrollToMonth
|
|
5251
|
+
}>
|
|
4687
5252
|
Month
|
|
4688
5253
|
</button>
|
|
4689
5254
|
<button class="toolbar-button" title="Today" @click=${
|
|
4690
|
-
|
|
4691
|
-
|
|
5255
|
+
this.scrollToToday
|
|
5256
|
+
}>
|
|
4692
5257
|
Today
|
|
4693
5258
|
</button>
|
|
4694
5259
|
<button ?disabled=${
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
5260
|
+
this.historyStack.length === 0 ||
|
|
5261
|
+
this.historyIndex >= this.historyStack.length - 1
|
|
5262
|
+
} class="toolbar-button" title="Back" @click=${this.goBack}>
|
|
4698
5263
|
←
|
|
4699
5264
|
</button>
|
|
4700
5265
|
<button ?disabled=${
|
|
4701
|
-
|
|
4702
|
-
|
|
5266
|
+
this.historyStack.length === 0 || this.historyIndex === 0
|
|
5267
|
+
} class="toolbar-button" title="Forward" @click=${
|
|
5268
|
+
this.goForward
|
|
5269
|
+
}>
|
|
4703
5270
|
→
|
|
4704
5271
|
</button>
|
|
4705
5272
|
<div class="toolbar-zoom">
|
|
5273
|
+
<span class="toolbar-slider-label">Width</span>
|
|
5274
|
+
<input
|
|
5275
|
+
type="range"
|
|
5276
|
+
class="toolbar-zoom-slider"
|
|
5277
|
+
min="${MIN_DAY_COLUMN_WIDTH}"
|
|
5278
|
+
max="${MAX_DAY_COLUMN_WIDTH}"
|
|
5279
|
+
.value=${this.minDayColumnWidth}
|
|
5280
|
+
@input=${this.onDayColumnWidthSliderChange}
|
|
5281
|
+
title="Adjust minimum day column width"
|
|
5282
|
+
/>
|
|
5283
|
+
</div>
|
|
5284
|
+
<div class="toolbar-zoom">
|
|
5285
|
+
<span class="toolbar-slider-label">Zoom</span>
|
|
4706
5286
|
<input
|
|
4707
5287
|
type="range"
|
|
4708
5288
|
class="toolbar-zoom-slider"
|
|
@@ -4723,9 +5303,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
4723
5303
|
title="Select theme"
|
|
4724
5304
|
>
|
|
4725
5305
|
${availableThemes.map(
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
5306
|
+
(theme) =>
|
|
5307
|
+
html`<option value="${theme.name}">${theme.label}</option>`,
|
|
5308
|
+
)}
|
|
4729
5309
|
</select>-->
|
|
4730
5310
|
<div class="toolbar-search">
|
|
4731
5311
|
<span class="toolbar-search-icon">🔍</span>
|