@luckydye/calendar 1.3.2 → 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 +2344 -2061
- package/package.json +7 -1
- package/src/ActiveCalendarStore.ts +88 -88
- package/src/CalDAVConfig.ts +611 -514
- package/src/CalDAVSource.ts +561 -466
- package/src/CalendarIntegration.ts +64 -47
- package/src/CalendarInternal.ts +645 -614
- package/src/CalendarLayer.ts +1 -0
- package/src/CalendarStorage.ts +51 -48
- package/src/CalendarView.ts +883 -507
- package/src/Color.ts +48 -54
- package/src/GoogleCalendarSource.ts +758 -662
- package/src/ICal.ts +420 -348
- package/src/InMemorySource.ts +56 -48
- package/src/IndexedDBStorage.ts +444 -398
- package/src/InhouseBookingSource.ts +614 -523
- 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 +153 -78
- 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,33 +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
|
-
|
|
22
|
-
|
|
27
|
+
import {
|
|
28
|
+
type EventRect,
|
|
29
|
+
type EventsState,
|
|
30
|
+
type PreviewEventData,
|
|
31
|
+
createEventsLayer,
|
|
32
|
+
} from "./layers/EventsLayer.js";
|
|
23
33
|
import { createGridLayer } from "./layers/GridLayer.js";
|
|
24
|
-
import { createEventsLayer, type EventRect, type EventsState } from "./layers/EventsLayer.js";
|
|
25
34
|
import { createTimeseriesHeatmapLayer } from "./layers/TimeseriesHeatmapLayer.js";
|
|
26
35
|
|
|
27
36
|
const MIN_DAY_HEIGHT = 50;
|
|
28
37
|
const MAX_DAY_HEIGHT = 3000; // 1px per minute
|
|
29
38
|
const LEFT_GUTTER_WIDTH = 60;
|
|
30
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;
|
|
31
45
|
|
|
32
46
|
export class CalendarViewElement extends LitElement {
|
|
33
47
|
static styles = css`
|
|
@@ -65,7 +79,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
65
79
|
bottom: 0;
|
|
66
80
|
left: 0;
|
|
67
81
|
right: 0;
|
|
68
|
-
z-index:
|
|
82
|
+
z-index: 200;
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
.toolbar::before {
|
|
@@ -160,6 +174,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
160
174
|
gap: 8px;
|
|
161
175
|
}
|
|
162
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
|
+
|
|
163
183
|
.toolbar-zoom-slider {
|
|
164
184
|
width: 100px;
|
|
165
185
|
height: 4px;
|
|
@@ -231,10 +251,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
231
251
|
position: absolute;
|
|
232
252
|
inset: 0;
|
|
233
253
|
overflow-y: overlay;
|
|
234
|
-
overflow-x:
|
|
254
|
+
overflow-x: auto;
|
|
235
255
|
z-index: 1;
|
|
236
256
|
cursor: default;
|
|
237
257
|
overflow-anchor: none;
|
|
258
|
+
touch-action: pan-x pan-y;
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
:host([scroll-lock]) .scroll-container {
|
|
@@ -742,6 +763,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
742
763
|
return this._dayHeight;
|
|
743
764
|
}
|
|
744
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
|
+
|
|
745
777
|
_scrollTop = 0;
|
|
746
778
|
set scrollTop(value) {
|
|
747
779
|
this._scrollTop = value;
|
|
@@ -749,7 +781,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
749
781
|
if (!this.scrollContainer || !this.scrollContent) return;
|
|
750
782
|
|
|
751
783
|
if (this.scrollContainer.scrollHeight < value) {
|
|
752
|
-
this.scrollContent.style.minHeight = value + window.innerHeight
|
|
784
|
+
this.scrollContent.style.minHeight = `${value + window.innerHeight}px`;
|
|
753
785
|
}
|
|
754
786
|
|
|
755
787
|
this.scrollContainer.scrollTop = value;
|
|
@@ -797,10 +829,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
797
829
|
|
|
798
830
|
const progress = Math.min((currentTime - startTime) / duration, 1);
|
|
799
831
|
|
|
800
|
-
this._dayHeight =
|
|
832
|
+
this._dayHeight =
|
|
833
|
+
startDayHeight +
|
|
834
|
+
(toDayHeight - startDayHeight) * easeOutExpo(progress);
|
|
801
835
|
this.updateWeekOffsets();
|
|
802
836
|
|
|
803
|
-
const currentFrac =
|
|
837
|
+
const currentFrac =
|
|
838
|
+
startFrac + (targetFrac - startFrac) * easeOutCubic(progress);
|
|
804
839
|
this._scrollTop = Math.max(0, currentFrac * this.totalHeight);
|
|
805
840
|
|
|
806
841
|
if (this.scrollContainer) {
|
|
@@ -808,8 +843,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
808
843
|
this.scrollContent &&
|
|
809
844
|
this.scrollContainer.scrollHeight < this._scrollTop
|
|
810
845
|
) {
|
|
811
|
-
this.scrollContent.style.minHeight =
|
|
812
|
-
this._scrollTop + window.innerHeight
|
|
846
|
+
this.scrollContent.style.minHeight = `${
|
|
847
|
+
this._scrollTop + window.innerHeight
|
|
848
|
+
}px`;
|
|
813
849
|
}
|
|
814
850
|
this.scrollContainer.scrollTop = this._scrollTop;
|
|
815
851
|
}
|
|
@@ -852,6 +888,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
852
888
|
this.renderCanvas();
|
|
853
889
|
}
|
|
854
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
|
+
|
|
855
902
|
viewportHeight = 0;
|
|
856
903
|
|
|
857
904
|
currentTime = new Date();
|
|
@@ -904,8 +951,13 @@ export class CalendarViewElement extends LitElement {
|
|
|
904
951
|
timeUpdateInterval: ReturnType<typeof setInterval> | null = null;
|
|
905
952
|
isExtendingRange = false; // Prevents concurrent range extensions
|
|
906
953
|
boundaryCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
954
|
+
wheelZoomSyncTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
907
955
|
lastWheelTime = 0; // Timestamp of last wheel event, used to distinguish wheel vs scrollbar drag
|
|
908
956
|
isCreatingEvent = false;
|
|
957
|
+
isPinchZooming = false;
|
|
958
|
+
pinchStartDistance = 0;
|
|
959
|
+
pinchStartHeight = MIN_DAY_HEIGHT;
|
|
960
|
+
pinchStartOriginY = 0;
|
|
909
961
|
eventCreationStart: { x: number; y: number } | null = null;
|
|
910
962
|
eventCreationEnd: { x: number; y: number } | null = null;
|
|
911
963
|
eventCreationShiftPressed = false;
|
|
@@ -926,28 +978,49 @@ export class CalendarViewElement extends LitElement {
|
|
|
926
978
|
resizingOriginalEnd: Date | null = null;
|
|
927
979
|
isResizingEvent = false;
|
|
928
980
|
|
|
929
|
-
_columnsPerRow = 7;
|
|
930
|
-
set columnsPerRow(value: number) {
|
|
931
|
-
const clamped = Math.max(1, Math.min(7, Math.floor(value)));
|
|
932
|
-
if (this._columnsPerRow !== clamped) {
|
|
933
|
-
this._columnsPerRow = clamped;
|
|
934
|
-
this.updateWeekOffsets();
|
|
935
|
-
this.renderCanvas();
|
|
936
|
-
this.requestUpdate();
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
981
|
get columnsPerRow(): number {
|
|
940
|
-
return
|
|
982
|
+
return 7;
|
|
941
983
|
}
|
|
942
984
|
|
|
943
985
|
get rowsPerWeek(): number {
|
|
944
|
-
return
|
|
986
|
+
return 1;
|
|
945
987
|
}
|
|
946
988
|
|
|
947
989
|
getDayVisualPosition(dayIndex: number): { row: number; col: number } {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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;
|
|
951
1024
|
}
|
|
952
1025
|
|
|
953
1026
|
getVisualPositionFromCoords(
|
|
@@ -957,9 +1030,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
957
1030
|
): { dayIndex: number; timeFraction: number; weekYOffset: number } | null {
|
|
958
1031
|
if (!this.scrollContainer) return null;
|
|
959
1032
|
|
|
960
|
-
const dayWidth = gridWidth / this.
|
|
1033
|
+
const dayWidth = gridWidth / this.columnsPerRow;
|
|
961
1034
|
const col = Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth);
|
|
962
|
-
if (col < 0 || col >= this.
|
|
1035
|
+
if (col < 0 || col >= this.columnsPerRow) return null;
|
|
963
1036
|
|
|
964
1037
|
const week = this.weeks.find(
|
|
965
1038
|
(w) => w.height > 0 && y >= w.yOffset && y < w.yOffset + w.height,
|
|
@@ -968,7 +1041,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
968
1041
|
|
|
969
1042
|
const rowHeight = this.dayHeight;
|
|
970
1043
|
const row = Math.floor((y - week.yOffset) / rowHeight);
|
|
971
|
-
const dayIndex = row * this.
|
|
1044
|
+
const dayIndex = row * this.columnsPerRow + col;
|
|
972
1045
|
if (dayIndex < 0 || dayIndex > 6) return null;
|
|
973
1046
|
|
|
974
1047
|
const timeFraction = ((y - week.yOffset) % rowHeight) / rowHeight;
|
|
@@ -1056,7 +1129,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1056
1129
|
if (saved) {
|
|
1057
1130
|
return Math.max(
|
|
1058
1131
|
MIN_DAY_HEIGHT,
|
|
1059
|
-
Math.min(MAX_DAY_HEIGHT, parseFloat(saved)),
|
|
1132
|
+
Math.min(MAX_DAY_HEIGHT, Number.parseFloat(saved)),
|
|
1060
1133
|
);
|
|
1061
1134
|
}
|
|
1062
1135
|
|
|
@@ -1067,6 +1140,25 @@ export class CalendarViewElement extends LitElement {
|
|
|
1067
1140
|
localStorage.setItem("calendar-dayHeight", this.dayHeight.toString());
|
|
1068
1141
|
}
|
|
1069
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
|
+
|
|
1070
1162
|
saveScrollPosition(): void {
|
|
1071
1163
|
// Save the date at the center of the viewport instead of pixel offset
|
|
1072
1164
|
const centerY = this.scrollTop + this.viewportHeight / 2;
|
|
@@ -1085,6 +1177,90 @@ export class CalendarViewElement extends LitElement {
|
|
|
1085
1177
|
}
|
|
1086
1178
|
}
|
|
1087
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
|
+
|
|
1088
1264
|
loadScrollPosition(): void {
|
|
1089
1265
|
const saved = localStorage.getItem("calendar-scrollDate");
|
|
1090
1266
|
if (saved) {
|
|
@@ -1118,9 +1294,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
1118
1294
|
CalendarInternal.isSameDay(d, now),
|
|
1119
1295
|
);
|
|
1120
1296
|
if (todayIndex >= 0) {
|
|
1121
|
-
const row = Math.floor(todayIndex / this._columnsPerRow);
|
|
1122
1297
|
const timeFraction = (now.getHours() + now.getMinutes() / 60) / 24;
|
|
1123
|
-
offsetInWeek =
|
|
1298
|
+
offsetInWeek = timeFraction;
|
|
1124
1299
|
}
|
|
1125
1300
|
}
|
|
1126
1301
|
|
|
@@ -1347,8 +1522,17 @@ export class CalendarViewElement extends LitElement {
|
|
|
1347
1522
|
window.removeEventListener("paste", this.onPaste);
|
|
1348
1523
|
window.removeEventListener("keydown", this.onKeyDown);
|
|
1349
1524
|
window.removeEventListener("keyup", this.onKeyUp);
|
|
1525
|
+
if (this.wheelZoomSyncTimeout) {
|
|
1526
|
+
clearTimeout(this.wheelZoomSyncTimeout);
|
|
1527
|
+
this.wheelZoomSyncTimeout = null;
|
|
1528
|
+
}
|
|
1350
1529
|
|
|
1351
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);
|
|
1352
1536
|
this.scrollContainer.removeEventListener(
|
|
1353
1537
|
"mouseleave",
|
|
1354
1538
|
this.onScrollContainerMouseLeave,
|
|
@@ -1401,7 +1585,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1401
1585
|
}
|
|
1402
1586
|
|
|
1403
1587
|
private requestDescriptionSummary(event: CalendarEvent): void {
|
|
1404
|
-
const description = sanitizeEventDescription(
|
|
1588
|
+
const description = sanitizeEventDescription(
|
|
1589
|
+
event.description ?? "",
|
|
1590
|
+
).trim();
|
|
1405
1591
|
if (description.length <= 200) return;
|
|
1406
1592
|
|
|
1407
1593
|
const targetKey = this.getDescriptionSummaryTargetKey(event);
|
|
@@ -1452,11 +1638,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
1452
1638
|
if (key !== this.descriptionSummaryTargetKey) return;
|
|
1453
1639
|
this.descriptionSummaryLoading = false;
|
|
1454
1640
|
this.descriptionSummaryError = message;
|
|
1455
|
-
if (
|
|
1456
|
-
this.descriptionSummaryText
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1641
|
+
if (
|
|
1642
|
+
!this.descriptionSummaryText &&
|
|
1643
|
+
this.selectedEventForDetail?.description
|
|
1644
|
+
) {
|
|
1645
|
+
this.descriptionSummaryText =
|
|
1646
|
+
this.selectedEventForDetail.description.replaceAll("\\n", "\n");
|
|
1460
1647
|
}
|
|
1461
1648
|
this.requestUpdate();
|
|
1462
1649
|
}
|
|
@@ -1472,18 +1659,44 @@ export class CalendarViewElement extends LitElement {
|
|
|
1472
1659
|
repositionEventDetailOverlay(): void {
|
|
1473
1660
|
if (!this.selectedEventForDetail || !this.selectedEventRect) return;
|
|
1474
1661
|
|
|
1475
|
-
const overlay = this.renderRoot.querySelector<HTMLElement>(
|
|
1662
|
+
const overlay = this.renderRoot.querySelector<HTMLElement>(
|
|
1663
|
+
".event-detail-overlay",
|
|
1664
|
+
);
|
|
1476
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
|
+
}
|
|
1477
1677
|
|
|
1678
|
+
const actualWidth = overlay.offsetWidth;
|
|
1478
1679
|
const actualHeight = overlay.offsetHeight;
|
|
1479
1680
|
const GAP = 8;
|
|
1681
|
+
const containerWidth = this.scrollContainer?.clientWidth || 800;
|
|
1480
1682
|
const containerHeight = this.scrollContainer?.clientHeight || 600;
|
|
1481
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));
|
|
1482
1694
|
const rawTop = this.selectedEventRect.y - this.scrollTop;
|
|
1483
1695
|
const minTop = GAP;
|
|
1484
1696
|
const maxTop = containerHeight - actualHeight - GAP;
|
|
1485
1697
|
const top = Math.max(minTop, Math.min(maxTop, rawTop));
|
|
1486
1698
|
|
|
1699
|
+
overlay.style.left = `${left}px`;
|
|
1487
1700
|
overlay.style.top = `${top}px`;
|
|
1488
1701
|
}
|
|
1489
1702
|
|
|
@@ -1500,14 +1713,24 @@ export class CalendarViewElement extends LitElement {
|
|
|
1500
1713
|
|
|
1501
1714
|
const self = this;
|
|
1502
1715
|
const eventsState: EventsState = {
|
|
1503
|
-
get events() {
|
|
1504
|
-
|
|
1716
|
+
get events() {
|
|
1717
|
+
return self.events;
|
|
1718
|
+
},
|
|
1719
|
+
get previewEvents() {
|
|
1720
|
+
return self.getPreviewEvents();
|
|
1721
|
+
},
|
|
1722
|
+
get hoveredEventId() {
|
|
1723
|
+
return self.hoveredEventId;
|
|
1724
|
+
},
|
|
1505
1725
|
isEventSelected: (event) => self.internal.isEventSelected(event),
|
|
1506
|
-
shouldRenderEventWithStripes: (event) =>
|
|
1726
|
+
shouldRenderEventWithStripes: (event) =>
|
|
1727
|
+
self.shouldRenderEventWithStripes(event),
|
|
1507
1728
|
getStripePatternCanvas: () => self.getStripePatternCanvas(),
|
|
1508
1729
|
};
|
|
1509
1730
|
const heatmapState = {
|
|
1510
|
-
get events() {
|
|
1731
|
+
get events() {
|
|
1732
|
+
return self.heatmapEvents;
|
|
1733
|
+
},
|
|
1511
1734
|
};
|
|
1512
1735
|
this.eventsLayer = createEventsLayer(eventsState);
|
|
1513
1736
|
this.layers = [
|
|
@@ -1520,14 +1743,30 @@ export class CalendarViewElement extends LitElement {
|
|
|
1520
1743
|
|
|
1521
1744
|
// Restore zoom level from localStorage
|
|
1522
1745
|
const savedDayHeight = this.loadDayHeight();
|
|
1523
|
-
if (savedDayHeight !==
|
|
1746
|
+
if (savedDayHeight !== MIN_DAY_HEIGHT) {
|
|
1524
1747
|
this.dayHeight = savedDayHeight;
|
|
1525
1748
|
}
|
|
1749
|
+
const savedMinDayColumnWidth = this.loadMinDayColumnWidth();
|
|
1750
|
+
if (savedMinDayColumnWidth !== DEFAULT_DAY_COLUMN_WIDTH) {
|
|
1751
|
+
this.minDayColumnWidth = savedMinDayColumnWidth;
|
|
1752
|
+
}
|
|
1526
1753
|
|
|
1527
1754
|
if (this.scrollContainer) {
|
|
1528
1755
|
this.scrollContainer.addEventListener("scroll", this.onScroll, {
|
|
1529
1756
|
passive: false,
|
|
1530
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
|
+
});
|
|
1531
1770
|
this.scrollContainer.addEventListener(
|
|
1532
1771
|
"mouseleave",
|
|
1533
1772
|
this.onScrollContainerMouseLeave,
|
|
@@ -1639,30 +1878,27 @@ export class CalendarViewElement extends LitElement {
|
|
|
1639
1878
|
|
|
1640
1879
|
updateWeekOffsets(): void {
|
|
1641
1880
|
let y = 0;
|
|
1642
|
-
const weekHeight = this.dayHeight
|
|
1881
|
+
const weekHeight = this.dayHeight;
|
|
1643
1882
|
|
|
1644
1883
|
if (this.filter) {
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
+
}
|
|
1652
1898
|
|
|
1653
|
-
for (const week of this.weeks) {
|
|
1899
|
+
for (const [index, week] of this.weeks.entries()) {
|
|
1654
1900
|
week.yOffset = y;
|
|
1655
|
-
|
|
1656
|
-
// Check if any day in this week overlaps any event range
|
|
1657
|
-
const weekStartTime = week.days[0]?.getTime() ?? 0;
|
|
1658
|
-
const weekEndTime = week.days[6]?.getTime() ?? 0;
|
|
1659
|
-
|
|
1660
|
-
// Quick check: skip if week is entirely outside all event ranges
|
|
1661
|
-
const hasEvents = eventRanges.some(
|
|
1662
|
-
(range) => range.end >= weekStartTime && range.start <= weekEndTime,
|
|
1663
|
-
);
|
|
1664
|
-
|
|
1665
|
-
week.height = hasEvents ? weekHeight : 0;
|
|
1901
|
+
week.height = visibleWeekIndexes.has(index) ? weekHeight : 0;
|
|
1666
1902
|
y += week.height;
|
|
1667
1903
|
}
|
|
1668
1904
|
} else {
|
|
@@ -1676,6 +1912,77 @@ export class CalendarViewElement extends LitElement {
|
|
|
1676
1912
|
this.totalHeight = y;
|
|
1677
1913
|
}
|
|
1678
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
|
+
|
|
1679
1986
|
handleResize(): void {
|
|
1680
1987
|
if (!this.canvas || !this.scrollContainer) return;
|
|
1681
1988
|
|
|
@@ -1684,8 +1991,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
1684
1991
|
|
|
1685
1992
|
this.rect = rect;
|
|
1686
1993
|
|
|
1687
|
-
|
|
1994
|
+
const canvasWidth = LEFT_GUTTER_WIDTH + this.getViewportGridWidth();
|
|
1995
|
+
this.canvas.width = canvasWidth * dpr;
|
|
1688
1996
|
this.canvas.height = rect.height * dpr;
|
|
1997
|
+
this.canvas.style.width = `${canvasWidth}px`;
|
|
1998
|
+
this.canvas.style.height = `${rect.height}px`;
|
|
1689
1999
|
this.viewportHeight = rect.height;
|
|
1690
2000
|
|
|
1691
2001
|
// Reset and rescale context (scale is reset when canvas dimensions change)
|
|
@@ -1696,7 +2006,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1696
2006
|
|
|
1697
2007
|
// Resize and configure overlay canvas
|
|
1698
2008
|
if (this.overlayCanvas) {
|
|
1699
|
-
const overlayWidth =
|
|
2009
|
+
const overlayWidth = this.getViewportGridWidth();
|
|
1700
2010
|
this.overlayCanvas.width = overlayWidth * dpr;
|
|
1701
2011
|
this.overlayCanvas.height = rect.height * dpr;
|
|
1702
2012
|
this.overlayCanvas.style.width = `${overlayWidth}px`;
|
|
@@ -1708,30 +2018,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1708
2018
|
}
|
|
1709
2019
|
}
|
|
1710
2020
|
|
|
1711
|
-
this.updateColumnsForViewport();
|
|
1712
2021
|
this.renderCanvas();
|
|
1713
2022
|
}
|
|
1714
2023
|
|
|
1715
|
-
updateColumnsForViewport(): void {
|
|
1716
|
-
if (!this.scrollContainer) return;
|
|
1717
|
-
|
|
1718
|
-
const rect = this.scrollContainer.getBoundingClientRect();
|
|
1719
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
1720
|
-
|
|
1721
|
-
// Determine optimal columns based on available width
|
|
1722
|
-
// Minimum 60px per day column for usability
|
|
1723
|
-
const minDayWidth = 120;
|
|
1724
|
-
const optimalColumns = Math.max(
|
|
1725
|
-
1,
|
|
1726
|
-
Math.min(7, Math.floor(gridWidth / minDayWidth)),
|
|
1727
|
-
);
|
|
1728
|
-
|
|
1729
|
-
if (this._columnsPerRow !== optimalColumns) {
|
|
1730
|
-
this._columnsPerRow = optimalColumns;
|
|
1731
|
-
this.updateWeekOffsets();
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
2024
|
resolveStyles(): Record<string, string> {
|
|
1736
2025
|
const cs = getComputedStyle(this);
|
|
1737
2026
|
const props = [
|
|
@@ -1761,7 +2050,10 @@ export class CalendarViewElement extends LitElement {
|
|
|
1761
2050
|
const ids = new Set<string>();
|
|
1762
2051
|
if (saved) {
|
|
1763
2052
|
try {
|
|
1764
|
-
const sources = JSON.parse(saved) as Array<{
|
|
2053
|
+
const sources = JSON.parse(saved) as Array<{
|
|
2054
|
+
id?: string;
|
|
2055
|
+
type?: string;
|
|
2056
|
+
}>;
|
|
1765
2057
|
for (const source of sources) {
|
|
1766
2058
|
if (source?.type === "timeseries-json" && source.id) {
|
|
1767
2059
|
ids.add(source.id);
|
|
@@ -1802,7 +2094,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1802
2094
|
const timeseriesIds = this.loadTimeseriesSourceIds();
|
|
1803
2095
|
const enabledKey = [...this.internal.enabledCalendars].join(",");
|
|
1804
2096
|
const lockedKey = [...this.internal.lockedCalendars].join(",");
|
|
1805
|
-
const key = `${start.toISOString()}::${end.toISOString()}::${
|
|
2097
|
+
const key = `${start.toISOString()}::${end.toISOString()}::${
|
|
2098
|
+
this.filter || ""
|
|
2099
|
+
}::${enabledKey}::${lockedKey}`;
|
|
1806
2100
|
if (!force && key === this.heatmapQueryKey) return;
|
|
1807
2101
|
this.heatmapQueryKey = key;
|
|
1808
2102
|
|
|
@@ -1838,8 +2132,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1838
2132
|
ctx.clearRect(0, 0, width, height);
|
|
1839
2133
|
|
|
1840
2134
|
const scrollTop = this.scrollTop;
|
|
1841
|
-
const
|
|
1842
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
2135
|
+
const dayWidth = this.getDayWidth();
|
|
1843
2136
|
|
|
1844
2137
|
const visibleWeeks = this.weeks.filter(
|
|
1845
2138
|
(w) =>
|
|
@@ -1855,10 +2148,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
1855
2148
|
width,
|
|
1856
2149
|
height,
|
|
1857
2150
|
scrollTop,
|
|
2151
|
+
scrollLeft: this.scrollLeft,
|
|
1858
2152
|
dayWidth,
|
|
1859
2153
|
dayHeight: this.dayHeight,
|
|
1860
2154
|
leftGutterWidth: LEFT_GUTTER_WIDTH,
|
|
1861
|
-
columnsPerRow: this.
|
|
2155
|
+
columnsPerRow: this.columnsPerRow,
|
|
1862
2156
|
rowsPerWeek: this.rowsPerWeek,
|
|
1863
2157
|
visibleWeeks,
|
|
1864
2158
|
allWeeks: this.weeks,
|
|
@@ -1875,6 +2169,14 @@ export class CalendarViewElement extends LitElement {
|
|
|
1875
2169
|
ctx.restore();
|
|
1876
2170
|
}
|
|
1877
2171
|
|
|
2172
|
+
this.renderTimeScaleGutter(
|
|
2173
|
+
ctx,
|
|
2174
|
+
visibleWeeks,
|
|
2175
|
+
height,
|
|
2176
|
+
fontFamily,
|
|
2177
|
+
lc.styles,
|
|
2178
|
+
);
|
|
2179
|
+
|
|
1878
2180
|
// Copy event rects from events layer for hit-testing
|
|
1879
2181
|
if (this.eventsLayer) {
|
|
1880
2182
|
this.eventRects = this.eventsLayer.eventRects;
|
|
@@ -1887,14 +2189,102 @@ export class CalendarViewElement extends LitElement {
|
|
|
1887
2189
|
// Draw sticky weekday labels at top of viewport
|
|
1888
2190
|
this.renderWeekdayLabels(ctx, dayWidth, visibleWeeks, scrollTop, height);
|
|
1889
2191
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
+
);
|
|
1895
2285
|
}
|
|
1896
2286
|
|
|
1897
|
-
|
|
2287
|
+
ctx.restore();
|
|
1898
2288
|
}
|
|
1899
2289
|
|
|
1900
2290
|
toggleLayer(name: string): void {
|
|
@@ -1904,7 +2294,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
1904
2294
|
this.renderCanvas();
|
|
1905
2295
|
}
|
|
1906
2296
|
|
|
1907
|
-
|
|
1908
2297
|
onWheel = (e: WheelEvent): void => {
|
|
1909
2298
|
if (this.hasAttribute("scroll-lock")) return;
|
|
1910
2299
|
this.lastWheelTime = Date.now();
|
|
@@ -1914,7 +2303,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
1914
2303
|
}
|
|
1915
2304
|
|
|
1916
2305
|
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
1917
|
-
const
|
|
2306
|
+
const isPinchZoomGesture = e.ctrlKey;
|
|
2307
|
+
const isShortcutZoom = isMac ? e.metaKey : e.ctrlKey;
|
|
2308
|
+
const isZoomGesture = isPinchZoomGesture || isShortcutZoom;
|
|
1918
2309
|
|
|
1919
2310
|
if (this.isFiltered && this.historyIndex === 0) {
|
|
1920
2311
|
this.replaceFilterInHistory();
|
|
@@ -1922,7 +2313,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
1922
2313
|
this.debouncedSaveToHistory();
|
|
1923
2314
|
}
|
|
1924
2315
|
|
|
1925
|
-
if (!
|
|
2316
|
+
if (!isZoomGesture || !this.scrollContainer) {
|
|
1926
2317
|
this.updateMousePosition();
|
|
1927
2318
|
return;
|
|
1928
2319
|
}
|
|
@@ -1942,8 +2333,95 @@ export class CalendarViewElement extends LitElement {
|
|
|
1942
2333
|
const newOriginY = this.zoomOriginY * scaleRatio;
|
|
1943
2334
|
const newScrollTop = newOriginY - this.zoomViewportY;
|
|
1944
2335
|
|
|
1945
|
-
|
|
2336
|
+
// Avoid DOM scroll sync during wheel zoom to prevent scroll/zoom race flicker.
|
|
2337
|
+
this.setView(newHeight, newScrollTop, false);
|
|
1946
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();
|
|
1947
2425
|
};
|
|
1948
2426
|
|
|
1949
2427
|
lastPointerY = 0;
|
|
@@ -2080,7 +2558,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2080
2558
|
this.scrollContainer
|
|
2081
2559
|
) {
|
|
2082
2560
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2083
|
-
const currentX = e.clientX
|
|
2561
|
+
const currentX = this.getContentXFromClientX(e.clientX);
|
|
2084
2562
|
const currentY = e.clientY - rect.top + this.scrollTop;
|
|
2085
2563
|
|
|
2086
2564
|
// Start selection
|
|
@@ -2102,7 +2580,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2102
2580
|
this.selection = {
|
|
2103
2581
|
startX: this.selectionStartX,
|
|
2104
2582
|
startY: this.selectionStartY,
|
|
2105
|
-
endX: e.clientX
|
|
2583
|
+
endX: this.getContentXFromClientX(e.clientX),
|
|
2106
2584
|
endY: e.clientY - rect.top + this.scrollTop,
|
|
2107
2585
|
};
|
|
2108
2586
|
this.requestUpdate();
|
|
@@ -2111,7 +2589,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2111
2589
|
// Event creation drag
|
|
2112
2590
|
if (this.eventCreationStart && this.scrollContainer) {
|
|
2113
2591
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2114
|
-
const currentX = e.clientX
|
|
2592
|
+
const currentX = this.getContentXFromClientX(e.clientX);
|
|
2115
2593
|
const currentY = e.clientY - rect.top + this.scrollTop;
|
|
2116
2594
|
this.isCreatingEvent = true;
|
|
2117
2595
|
|
|
@@ -2153,7 +2631,6 @@ export class CalendarViewElement extends LitElement {
|
|
|
2153
2631
|
this.eventCreationEnd = { x: currentX, y: currentY };
|
|
2154
2632
|
this.eventCreationPreviousShiftPressed = e.shiftKey;
|
|
2155
2633
|
this.renderDateLabels();
|
|
2156
|
-
this.renderEventCreationPreview();
|
|
2157
2634
|
}
|
|
2158
2635
|
|
|
2159
2636
|
// Event move drag - now handled by HTML5 drag and drop
|
|
@@ -2165,7 +2642,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2165
2642
|
// Event resize drag
|
|
2166
2643
|
if (this.resizingEvent && this.resizingEdge && this.scrollContainer) {
|
|
2167
2644
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2168
|
-
const currentX = e.clientX
|
|
2645
|
+
const currentX = this.getContentXFromClientX(e.clientX);
|
|
2169
2646
|
const currentY = e.clientY - rect.top + this.scrollTop;
|
|
2170
2647
|
this.isResizingEvent = true;
|
|
2171
2648
|
|
|
@@ -2219,14 +2696,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2219
2696
|
}
|
|
2220
2697
|
if (this.isDraggingZoom) {
|
|
2221
2698
|
this.isDraggingZoom = false;
|
|
2222
|
-
|
|
2223
|
-
if (this.scrollContainer && this.scrollContent) {
|
|
2224
|
-
if (this.scrollContainer.scrollHeight < this._scrollTop) {
|
|
2225
|
-
this.scrollContent.style.minHeight =
|
|
2226
|
-
this._scrollTop + window.innerHeight + "px";
|
|
2227
|
-
}
|
|
2228
|
-
this.scrollContainer.scrollTop = this._scrollTop;
|
|
2229
|
-
}
|
|
2699
|
+
this.syncScrollDomToState();
|
|
2230
2700
|
}
|
|
2231
2701
|
if (this.isDraggingMinimap) {
|
|
2232
2702
|
this.isDraggingMinimap = false;
|
|
@@ -2298,7 +2768,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
2298
2768
|
const isZoomedOut = this.dayHeight < TIME_SCALE_DAY_HEIGHT;
|
|
2299
2769
|
this.dispatchEvent(
|
|
2300
2770
|
new CustomEvent("create-event", {
|
|
2301
|
-
detail: {
|
|
2771
|
+
detail: {
|
|
2772
|
+
start: snappedStart,
|
|
2773
|
+
end: snappedEnd,
|
|
2774
|
+
isAllDay: isZoomedOut,
|
|
2775
|
+
},
|
|
2302
2776
|
bubbles: true,
|
|
2303
2777
|
}),
|
|
2304
2778
|
);
|
|
@@ -2486,14 +2960,15 @@ export class CalendarViewElement extends LitElement {
|
|
|
2486
2960
|
if (!this.scrollContainer || this.isDraggingZoom) return;
|
|
2487
2961
|
|
|
2488
2962
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2489
|
-
const
|
|
2963
|
+
const viewportX = e.clientX - rect.left;
|
|
2964
|
+
const contentX = this.getContentXFromClientX(e.clientX);
|
|
2490
2965
|
const y = e.clientY - rect.top + this.scrollTop;
|
|
2491
2966
|
|
|
2492
2967
|
// Update cursor position for status bar
|
|
2493
|
-
this.cursorPosition = { x, y };
|
|
2968
|
+
this.cursorPosition = { x: contentX, y };
|
|
2494
2969
|
|
|
2495
2970
|
// Check for resize handles first — suppressed when alt key is active
|
|
2496
|
-
const resizeHandle = !e.altKey ? this.getResizeHandle(
|
|
2971
|
+
const resizeHandle = !e.altKey ? this.getResizeHandle(viewportX, y) : null;
|
|
2497
2972
|
if (resizeHandle && !this.isResizingEvent && !this.isDraggingEvent) {
|
|
2498
2973
|
this.scrollContainer.style.cursor =
|
|
2499
2974
|
this.dayHeight >= TIME_SCALE_DAY_HEIGHT ? "ns-resize" : "ew-resize";
|
|
@@ -2502,7 +2977,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
2502
2977
|
}
|
|
2503
2978
|
|
|
2504
2979
|
// Check for event hover — suppressed when alt key is active
|
|
2505
|
-
const hoveredEvent = !e.altKey
|
|
2980
|
+
const hoveredEvent = !e.altKey
|
|
2981
|
+
? this.getEventAtPosition(viewportX, y)
|
|
2982
|
+
: null;
|
|
2506
2983
|
const newHoveredId = hoveredEvent ? hoveredEvent.id : null;
|
|
2507
2984
|
|
|
2508
2985
|
if (newHoveredId !== this.hoveredEventId) {
|
|
@@ -2512,7 +2989,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2512
2989
|
|
|
2513
2990
|
this.requestUpdate();
|
|
2514
2991
|
|
|
2515
|
-
if (
|
|
2992
|
+
if (viewportX < LEFT_GUTTER_WIDTH) {
|
|
2516
2993
|
this.scrollContainer.classList.add("zoom-cursor");
|
|
2517
2994
|
} else {
|
|
2518
2995
|
this.scrollContainer.classList.remove("zoom-cursor");
|
|
@@ -2557,7 +3034,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2557
3034
|
// Update visual preview
|
|
2558
3035
|
if (this.scrollContainer) {
|
|
2559
3036
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2560
|
-
const x = e.clientX
|
|
3037
|
+
const x = this.getContentXFromClientX(e.clientX);
|
|
2561
3038
|
const y = e.clientY - rect.top + this.scrollTop;
|
|
2562
3039
|
this.movingEventEnd = { x, y };
|
|
2563
3040
|
this.renderCanvas(); // Re-render canvas with preview
|
|
@@ -2601,7 +3078,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
2601
3078
|
) {
|
|
2602
3079
|
// Handle internal event drop
|
|
2603
3080
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2604
|
-
const dropX = e.clientX
|
|
3081
|
+
const dropX = this.getContentXFromClientX(e.clientX);
|
|
2605
3082
|
const dropY = e.clientY - rect.top + this.scrollTop;
|
|
2606
3083
|
|
|
2607
3084
|
const originDate = this.convertPositionToDateTime(
|
|
@@ -2661,7 +3138,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
2661
3138
|
|
|
2662
3139
|
onKeyDown = (e: KeyboardEvent): void => {
|
|
2663
3140
|
const focused = e.composedPath()[0] as HTMLElement;
|
|
2664
|
-
if (
|
|
3141
|
+
if (
|
|
3142
|
+
focused?.tagName === "INPUT" ||
|
|
3143
|
+
focused?.tagName === "TEXTAREA" ||
|
|
3144
|
+
focused?.isContentEditable
|
|
3145
|
+
)
|
|
3146
|
+
return;
|
|
2665
3147
|
if (e.altKey && !this.altKeyActive) {
|
|
2666
3148
|
this.altKeyActive = true;
|
|
2667
3149
|
this.dispatchEvent(new CustomEvent("meta-key-change", { bubbles: true }));
|
|
@@ -2677,7 +3159,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
2677
3159
|
|
|
2678
3160
|
onKeyUp = (e: KeyboardEvent): void => {
|
|
2679
3161
|
const focused = e.composedPath()[0] as HTMLElement;
|
|
2680
|
-
if (
|
|
3162
|
+
if (
|
|
3163
|
+
focused?.tagName === "INPUT" ||
|
|
3164
|
+
focused?.tagName === "TEXTAREA" ||
|
|
3165
|
+
focused?.isContentEditable
|
|
3166
|
+
)
|
|
3167
|
+
return;
|
|
2681
3168
|
if (!e.altKey && this.altKeyActive) {
|
|
2682
3169
|
this.altKeyActive = false;
|
|
2683
3170
|
this.dispatchEvent(new CustomEvent("meta-key-change", { bubbles: true }));
|
|
@@ -2857,17 +3344,18 @@ export class CalendarViewElement extends LitElement {
|
|
|
2857
3344
|
}
|
|
2858
3345
|
|
|
2859
3346
|
const rect = this.scrollContainer.getBoundingClientRect();
|
|
2860
|
-
const
|
|
3347
|
+
const viewportX = e.clientX - rect.left;
|
|
3348
|
+
const x = this.getContentXFromClientX(e.clientX);
|
|
2861
3349
|
const y = e.clientY - rect.top + this.scrollTop;
|
|
2862
3350
|
|
|
2863
3351
|
// Check if clicking on zoom handle area (left gutter)
|
|
2864
|
-
if (
|
|
3352
|
+
if (viewportX < LEFT_GUTTER_WIDTH) {
|
|
2865
3353
|
this.onZoomHandleMouseDown(e);
|
|
2866
3354
|
return;
|
|
2867
3355
|
}
|
|
2868
3356
|
|
|
2869
3357
|
// Check if clicking on a resize handle first
|
|
2870
|
-
const resizeHandle = !e.altKey ? this.getResizeHandle(
|
|
3358
|
+
const resizeHandle = !e.altKey ? this.getResizeHandle(viewportX, y) : null;
|
|
2871
3359
|
if (resizeHandle) {
|
|
2872
3360
|
this.resizingEvent = resizeHandle.event;
|
|
2873
3361
|
this.resizingEdge = resizeHandle.edge;
|
|
@@ -2881,7 +3369,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
2881
3369
|
|
|
2882
3370
|
// Check if clicking on an event — defer click vs drag to mouseup
|
|
2883
3371
|
// Skip event interaction if Alt/Option is held (create new event instead)
|
|
2884
|
-
const clickedEvent = !e.altKey
|
|
3372
|
+
const clickedEvent = !e.altKey
|
|
3373
|
+
? this.getEventAtPosition(viewportX, y)
|
|
3374
|
+
: null;
|
|
2885
3375
|
if (clickedEvent) {
|
|
2886
3376
|
// Clear selection when starting to drag/move an event
|
|
2887
3377
|
const hadSelection = this.internal.getSelectedEvents().length > 0;
|
|
@@ -2973,22 +3463,20 @@ export class CalendarViewElement extends LitElement {
|
|
|
2973
3463
|
const minY = Math.min(this.selection.startY, this.selection.endY);
|
|
2974
3464
|
const maxY = Math.max(this.selection.startY, this.selection.endY);
|
|
2975
3465
|
|
|
2976
|
-
const
|
|
2977
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
2978
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
3466
|
+
const dayWidth = this.getDayWidth();
|
|
2979
3467
|
|
|
2980
3468
|
// Find column indices from X coordinates
|
|
2981
3469
|
const startCol = Math.max(
|
|
2982
3470
|
0,
|
|
2983
3471
|
Math.min(
|
|
2984
|
-
this.
|
|
3472
|
+
this.columnsPerRow - 1,
|
|
2985
3473
|
Math.floor((minX - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
2986
3474
|
),
|
|
2987
3475
|
);
|
|
2988
3476
|
const endCol = Math.max(
|
|
2989
3477
|
0,
|
|
2990
3478
|
Math.min(
|
|
2991
|
-
this.
|
|
3479
|
+
this.columnsPerRow - 1,
|
|
2992
3480
|
Math.floor((maxX - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
2993
3481
|
),
|
|
2994
3482
|
);
|
|
@@ -3093,7 +3581,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
3093
3581
|
if (event.visualStyle === "heatmap") continue;
|
|
3094
3582
|
|
|
3095
3583
|
// Only select events from the active calendar
|
|
3096
|
-
if (
|
|
3584
|
+
if (
|
|
3585
|
+
this.activeCalendarId &&
|
|
3586
|
+
event.calendarId !== this.activeCalendarId &&
|
|
3587
|
+
event.sourceId !== this.activeCalendarId
|
|
3588
|
+
)
|
|
3589
|
+
continue;
|
|
3097
3590
|
|
|
3098
3591
|
const eventStartTime = event.start.getTime();
|
|
3099
3592
|
const eventEndTime = event.end.getTime();
|
|
@@ -3301,7 +3794,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3301
3794
|
|
|
3302
3795
|
onZoomSliderChange = (e: Event): void => {
|
|
3303
3796
|
const slider = e.target as HTMLInputElement;
|
|
3304
|
-
const newHeight = parseInt(slider.value, 10);
|
|
3797
|
+
const newHeight = Number.parseInt(slider.value, 10);
|
|
3305
3798
|
const oldHeight = this.dayHeight;
|
|
3306
3799
|
|
|
3307
3800
|
if (!this.scrollContainer) {
|
|
@@ -3321,6 +3814,38 @@ export class CalendarViewElement extends LitElement {
|
|
|
3321
3814
|
this.setView(newHeight, newScrollTop);
|
|
3322
3815
|
};
|
|
3323
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
|
+
|
|
3324
3849
|
async onEventClick(event: CalendarEvent, e: MouseEvent): Promise<void> {
|
|
3325
3850
|
const isCmdOrCtrl = e.metaKey || e.ctrlKey;
|
|
3326
3851
|
|
|
@@ -3468,296 +3993,135 @@ export class CalendarViewElement extends LitElement {
|
|
|
3468
3993
|
this.renderDateLabels();
|
|
3469
3994
|
}
|
|
3470
3995
|
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
return;
|
|
3474
|
-
|
|
3475
|
-
const originDate = this.convertPositionToDateTime(
|
|
3476
|
-
this.movingEventOrigin.x,
|
|
3477
|
-
this.movingEventOrigin.y,
|
|
3478
|
-
);
|
|
3479
|
-
const currentDate = this.convertPositionToDateTime(
|
|
3480
|
-
this.movingEventEnd.x,
|
|
3481
|
-
this.movingEventEnd.y,
|
|
3482
|
-
);
|
|
3483
|
-
if (!originDate || !currentDate) return;
|
|
3996
|
+
getPreviewEvents(): PreviewEventData[] {
|
|
3997
|
+
const previews: PreviewEventData[] = [];
|
|
3484
3998
|
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
};
|
|
3490
|
-
const newStart = snap15(
|
|
3491
|
-
new Date(this.movingEvent.start.getTime() + deltaMs),
|
|
3492
|
-
);
|
|
3493
|
-
const newEnd = snap15(new Date(this.movingEvent.end.getTime() + deltaMs));
|
|
3494
|
-
|
|
3495
|
-
// The grab point (originDate) relative to the event's midpoint tells us
|
|
3496
|
-
// which half the user grabbed — this is stable for the whole drag.
|
|
3497
|
-
const eventMidMs = (this.movingEvent.start.getTime() + this.movingEvent.end.getTime()) / 2;
|
|
3498
|
-
const useStartEdge = originDate.getTime() <= eventMidMs;
|
|
3499
|
-
|
|
3500
|
-
if (this.movingEventDuplicateMode) {
|
|
3501
|
-
const accent =
|
|
3502
|
-
getComputedStyle(this).getPropertyValue("--accent-primary").trim() ||
|
|
3503
|
-
"rgb(100, 150, 255)";
|
|
3504
|
-
this.renderVirtualEvent(
|
|
3505
|
-
newStart,
|
|
3506
|
-
newEnd,
|
|
3507
|
-
{
|
|
3508
|
-
fill: accent.replace("rgb", "rgba").replace(")", ", 0.3)"),
|
|
3509
|
-
stroke: accent.replace("rgb", "rgba").replace(")", ", 0.8)"),
|
|
3510
|
-
text: "white",
|
|
3511
|
-
dashed: false,
|
|
3512
|
-
},
|
|
3513
|
-
useStartEdge,
|
|
3514
|
-
);
|
|
3515
|
-
} else {
|
|
3516
|
-
const rgb = hexToRgb(this.movingEvent.color || "#888888");
|
|
3517
|
-
this.renderVirtualEvent(
|
|
3518
|
-
newStart,
|
|
3519
|
-
newEnd,
|
|
3520
|
-
{
|
|
3521
|
-
fill: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
3522
|
-
stroke: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.5)`,
|
|
3523
|
-
text: "rgba(255, 255, 255, 0.5)",
|
|
3524
|
-
dashed: false,
|
|
3525
|
-
},
|
|
3526
|
-
useStartEdge,
|
|
3999
|
+
if (this.movingEvent && this.movingEventOrigin && this.movingEventEnd) {
|
|
4000
|
+
const originDate = this.convertPositionToDateTime(
|
|
4001
|
+
this.movingEventOrigin.x,
|
|
4002
|
+
this.movingEventOrigin.y,
|
|
3527
4003
|
);
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3531
|
-
renderEventCreationPreview(): void {
|
|
3532
|
-
if (!this.eventCreationStart || !this.eventCreationEnd) return;
|
|
3533
|
-
|
|
3534
|
-
const startDate = this.convertPositionToDateTime(
|
|
3535
|
-
this.eventCreationStart.x,
|
|
3536
|
-
this.eventCreationStart.y,
|
|
3537
|
-
);
|
|
3538
|
-
const endDate = this.convertPositionToDateTime(
|
|
3539
|
-
this.eventCreationEnd.x,
|
|
3540
|
-
this.eventCreationEnd.y,
|
|
3541
|
-
);
|
|
3542
|
-
if (!startDate || !endDate) return;
|
|
3543
|
-
|
|
3544
|
-
let earlier: Date;
|
|
3545
|
-
let later: Date;
|
|
3546
|
-
|
|
3547
|
-
if (
|
|
3548
|
-
this.eventCreationShiftPressed &&
|
|
3549
|
-
this.eventCreationInitialDuration !== null
|
|
3550
|
-
) {
|
|
3551
|
-
// Shift pressed: move event with fixed duration to current mouse position
|
|
3552
4004
|
const currentDate = this.convertPositionToDateTime(
|
|
3553
|
-
this.
|
|
3554
|
-
this.
|
|
4005
|
+
this.movingEventEnd.x,
|
|
4006
|
+
this.movingEventEnd.y,
|
|
3555
4007
|
);
|
|
3556
|
-
if (
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
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
|
+
}
|
|
3568
4022
|
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
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
|
+
}
|
|
3575
4055
|
}
|
|
3576
4056
|
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
const rgb = hexToRgb(this.activeCalendarColor);
|
|
3582
|
-
fill = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.3)`;
|
|
3583
|
-
stroke = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.8)`;
|
|
3584
|
-
} else {
|
|
3585
|
-
const accent =
|
|
3586
|
-
getComputedStyle(this).getPropertyValue("--accent-primary").trim() ||
|
|
3587
|
-
"rgb(100, 150, 255)";
|
|
3588
|
-
fill = accent.replace("rgb", "rgba").replace(")", ", 0.3)");
|
|
3589
|
-
stroke = accent.replace("rgb", "rgba").replace(")", ", 0.8)");
|
|
3590
|
-
}
|
|
3591
|
-
// In shift mode the cursor tracks the end; otherwise it tracks whichever
|
|
3592
|
-
// edge is away from the fixed anchor (eventCreationStart).
|
|
3593
|
-
const useStartEdge = this.eventCreationShiftPressed
|
|
3594
|
-
? false
|
|
3595
|
-
: this.eventCreationEnd.y <= this.eventCreationStart.y;
|
|
3596
|
-
|
|
3597
|
-
this.renderVirtualEvent(earlier, later, { fill, stroke, text: "white" }, useStartEdge);
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
renderVirtualEvent(
|
|
3601
|
-
start: Date,
|
|
3602
|
-
end: Date,
|
|
3603
|
-
color: { fill: string; stroke: string; text: string; dashed?: boolean },
|
|
3604
|
-
useStartEdge = true,
|
|
3605
|
-
): void {
|
|
3606
|
-
if (!this.overlayCanvas || !this.overlayCtx || !this.scrollContainer)
|
|
3607
|
-
return;
|
|
3608
|
-
|
|
3609
|
-
const ctx = this.overlayCtx;
|
|
3610
|
-
const fontFamily = getComputedStyle(this).fontFamily;
|
|
3611
|
-
const scrollRect = this.scrollContainer.getBoundingClientRect();
|
|
3612
|
-
const gridWidth = scrollRect.width - LEFT_GUTTER_WIDTH - MINIMAP_WIDTH;
|
|
3613
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
3614
|
-
|
|
3615
|
-
const getTimeY = (day: Date, hours: number, minutes: number) => {
|
|
3616
|
-
const week = this.weeks.find((w) =>
|
|
3617
|
-
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,
|
|
3618
4061
|
);
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
);
|
|
3623
|
-
if (dayIndex < 0) return null;
|
|
3624
|
-
const { row } = this.getDayVisualPosition(dayIndex);
|
|
3625
|
-
const totalMinutes = hours * 60 + minutes;
|
|
3626
|
-
const rowY = week.yOffset + row * this.dayHeight;
|
|
3627
|
-
return rowY + (totalMinutes / 1440) * this.dayHeight - this.scrollTop;
|
|
3628
|
-
};
|
|
3629
|
-
|
|
3630
|
-
const getDayColumnX = (day: Date) => {
|
|
3631
|
-
const week = this.weeks.find((w) =>
|
|
3632
|
-
w.days.some((d) => d.toDateString() === day.toDateString()),
|
|
3633
|
-
);
|
|
3634
|
-
if (!week) return null;
|
|
3635
|
-
const dayIndex = week.days.findIndex(
|
|
3636
|
-
(d) => d.toDateString() === day.toDateString(),
|
|
4062
|
+
const endDate = this.convertPositionToDateTime(
|
|
4063
|
+
this.eventCreationEnd.x,
|
|
4064
|
+
this.eventCreationEnd.y,
|
|
3637
4065
|
);
|
|
3638
|
-
if (
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
};
|
|
3642
|
-
|
|
3643
|
-
const drawBlock = (colX: number, top: number, bottom: number) => {
|
|
3644
|
-
const bx = colX + 2;
|
|
3645
|
-
const bw = dayWidth - 4;
|
|
3646
|
-
const bh = Math.max(4, bottom - top);
|
|
3647
|
-
ctx.fillStyle = color.fill;
|
|
3648
|
-
ctx.beginPath();
|
|
3649
|
-
ctx.roundRect(bx, top, bw, bh, 4);
|
|
3650
|
-
ctx.fill();
|
|
3651
|
-
ctx.strokeStyle = color.stroke;
|
|
3652
|
-
ctx.lineWidth = 1;
|
|
3653
|
-
if (color.dashed !== false) ctx.setLineDash([6, 3]);
|
|
3654
|
-
ctx.stroke();
|
|
3655
|
-
ctx.setLineDash([]);
|
|
3656
|
-
};
|
|
3657
|
-
|
|
3658
|
-
const sameDay = start.toDateString() === end.toDateString();
|
|
4066
|
+
if (startDate && endDate) {
|
|
4067
|
+
let earlier: Date;
|
|
4068
|
+
let later: Date;
|
|
3659
4069
|
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
if (colX != null) {
|
|
3674
|
-
const isFirst = current.toDateString() === start.toDateString();
|
|
3675
|
-
const isLast = current.toDateString() === end.toDateString();
|
|
3676
|
-
let top: number | null;
|
|
3677
|
-
let bottom: number | null;
|
|
3678
|
-
if (isFirst) {
|
|
3679
|
-
top = getTimeY(current, start.getHours(), start.getMinutes());
|
|
3680
|
-
bottom = getTimeY(current, 23, 59);
|
|
3681
|
-
} else if (isLast) {
|
|
3682
|
-
top = getTimeY(current, 0, 0);
|
|
3683
|
-
bottom = getTimeY(current, end.getHours(), end.getMinutes());
|
|
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
|
+
);
|
|
3684
4083
|
} else {
|
|
3685
|
-
|
|
3686
|
-
|
|
4084
|
+
earlier = startDate < endDate ? startDate : endDate;
|
|
4085
|
+
later = startDate < endDate ? endDate : startDate;
|
|
3687
4086
|
}
|
|
3688
|
-
|
|
4087
|
+
} else {
|
|
4088
|
+
earlier = startDate < endDate ? startDate : endDate;
|
|
4089
|
+
later = startDate < endDate ? endDate : startDate;
|
|
3689
4090
|
}
|
|
3690
|
-
current.setDate(current.getDate() + 1);
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
// Time label
|
|
3695
|
-
const fmtTime = (d: Date) =>
|
|
3696
|
-
`${d.getHours().toString().padStart(2, "0")}:${d
|
|
3697
|
-
.getMinutes()
|
|
3698
|
-
.toString()
|
|
3699
|
-
.padStart(2, "0")}`;
|
|
3700
|
-
const fmtDate = (d: Date) => `${d.getDate()}.${d.getMonth() + 1}`;
|
|
3701
|
-
const label = sameDay
|
|
3702
|
-
? `${fmtTime(start)} – ${fmtTime(end)}`
|
|
3703
|
-
: `${fmtDate(start)} ${fmtTime(start)} – ${fmtDate(end)} ${fmtTime(end)}`;
|
|
3704
4091
|
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
const endY = getTimeY(end, end.getHours(), end.getMinutes());
|
|
3713
|
-
|
|
3714
|
-
if (
|
|
3715
|
-
firstColX != null &&
|
|
3716
|
-
lastColX != null &&
|
|
3717
|
-
startY != null &&
|
|
3718
|
-
endY != null
|
|
3719
|
-
) {
|
|
3720
|
-
ctx.font = `600 11px ${fontFamily}`;
|
|
3721
|
-
ctx.fillStyle = color.text;
|
|
3722
|
-
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
|
+
}
|
|
3723
4099
|
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
"rgba(0, 0, 0, 0.8)";
|
|
3739
|
-
ctx.fillStyle = bgElevated;
|
|
3740
|
-
ctx.beginPath();
|
|
3741
|
-
ctx.roundRect(
|
|
3742
|
-
labelColX + 4,
|
|
3743
|
-
labelBgY,
|
|
3744
|
-
textWidth + bgPaddingX * 2,
|
|
3745
|
-
16,
|
|
3746
|
-
4,
|
|
3747
|
-
);
|
|
3748
|
-
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
|
+
}
|
|
3749
4114
|
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
ctx.textBaseline = "top";
|
|
3757
|
-
const labelY = useStartEdge ? startY + 4 : endY - 18;
|
|
3758
|
-
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
|
+
});
|
|
3759
4121
|
}
|
|
3760
4122
|
}
|
|
4123
|
+
|
|
4124
|
+
return previews;
|
|
3761
4125
|
}
|
|
3762
4126
|
|
|
3763
4127
|
renderDateLabels(): void {
|
|
@@ -3770,11 +4134,12 @@ export class CalendarViewElement extends LitElement {
|
|
|
3770
4134
|
|
|
3771
4135
|
ctx.clearRect(0, 0, width, height);
|
|
3772
4136
|
|
|
3773
|
-
const dayWidth =
|
|
4137
|
+
const dayWidth = this.getDayWidth();
|
|
3774
4138
|
const scrollTop = this.scrollTop;
|
|
4139
|
+
const scrollLeft = this.scrollLeft;
|
|
3775
4140
|
const fontFamily = getComputedStyle(this).fontFamily;
|
|
3776
4141
|
|
|
3777
|
-
ctx.font = `600
|
|
4142
|
+
ctx.font = `600 12px ${fontFamily}`;
|
|
3778
4143
|
const textSecondary =
|
|
3779
4144
|
getComputedStyle(this).getPropertyValue("--text-secondary").trim() ||
|
|
3780
4145
|
"rgba(255, 255, 255, 0.6)";
|
|
@@ -3792,7 +4157,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
3792
4157
|
if (!day) continue;
|
|
3793
4158
|
|
|
3794
4159
|
const { row, col } = this.getDayVisualPosition(dayIndex);
|
|
3795
|
-
const x = col * dayWidth;
|
|
4160
|
+
const x = col * dayWidth - scrollLeft;
|
|
3796
4161
|
const dayTop = week.yOffset + row * this.dayHeight - scrollTop;
|
|
3797
4162
|
const dayBottom = dayTop + this.dayHeight;
|
|
3798
4163
|
|
|
@@ -3831,6 +4196,8 @@ export class CalendarViewElement extends LitElement {
|
|
|
3831
4196
|
}
|
|
3832
4197
|
}
|
|
3833
4198
|
}
|
|
4199
|
+
|
|
4200
|
+
this.renderMonthLabels(ctx, width, height, fontFamily);
|
|
3834
4201
|
}
|
|
3835
4202
|
|
|
3836
4203
|
renderWeekdayLabels(
|
|
@@ -3851,57 +4218,41 @@ export class CalendarViewElement extends LitElement {
|
|
|
3851
4218
|
getComputedStyle(this).getPropertyValue("--bg-primary").trim() ||
|
|
3852
4219
|
"rgba(30, 30, 30, 0.9)";
|
|
3853
4220
|
|
|
3854
|
-
ctx.font = `
|
|
4221
|
+
ctx.font = `600 12px ${fontFamily}`;
|
|
3855
4222
|
ctx.textAlign = "center";
|
|
3856
4223
|
ctx.textBaseline = "top";
|
|
3857
4224
|
|
|
3858
4225
|
const labelHeight = 16;
|
|
3859
|
-
const labelY =
|
|
4226
|
+
const labelY = 48; // Below sticky month label
|
|
3860
4227
|
|
|
3861
|
-
|
|
3862
|
-
const firstWeek = visibleWeeks[0];
|
|
3863
|
-
if (!firstWeek) return;
|
|
4228
|
+
const stickyY = labelY;
|
|
3864
4229
|
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
const rowBottom = rowTop + this.dayHeight;
|
|
4230
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
4231
|
+
const dayName = weekdayNames[dayIndex];
|
|
4232
|
+
if (!dayName) continue;
|
|
3869
4233
|
|
|
3870
|
-
|
|
3871
|
-
|
|
4234
|
+
const x =
|
|
4235
|
+
LEFT_GUTTER_WIDTH +
|
|
4236
|
+
dayIndex * dayWidth +
|
|
4237
|
+
dayWidth / 2 -
|
|
4238
|
+
this.scrollLeft;
|
|
3872
4239
|
|
|
3873
|
-
|
|
3874
|
-
const
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
// Draw background pill
|
|
3887
|
-
const textWidth = ctx.measureText(dayName).width;
|
|
3888
|
-
const bgPaddingX = 6;
|
|
3889
|
-
const bgPaddingY = 2;
|
|
3890
|
-
ctx.fillStyle = bgPrimary;
|
|
3891
|
-
ctx.beginPath();
|
|
3892
|
-
ctx.roundRect(
|
|
3893
|
-
x - textWidth / 2 - bgPaddingX,
|
|
3894
|
-
stickyY,
|
|
3895
|
-
textWidth + bgPaddingX * 2,
|
|
3896
|
-
labelHeight,
|
|
3897
|
-
4,
|
|
3898
|
-
);
|
|
3899
|
-
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();
|
|
3900
4253
|
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
|
|
3904
|
-
}
|
|
4254
|
+
ctx.fillStyle = textMuted;
|
|
4255
|
+
ctx.fillText(dayName, x, stickyY + bgPaddingY + 1);
|
|
3905
4256
|
}
|
|
3906
4257
|
}
|
|
3907
4258
|
|
|
@@ -4099,9 +4450,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4099
4450
|
): Date | null {
|
|
4100
4451
|
if (!this.scrollContainer) return null;
|
|
4101
4452
|
|
|
4102
|
-
const
|
|
4103
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
4104
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4453
|
+
const dayWidth = this.getDayWidth();
|
|
4105
4454
|
|
|
4106
4455
|
// Check if X is in the calendar grid area
|
|
4107
4456
|
if (x < LEFT_GUTTER_WIDTH) return null;
|
|
@@ -4109,7 +4458,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4109
4458
|
const col = Math.max(
|
|
4110
4459
|
0,
|
|
4111
4460
|
Math.min(
|
|
4112
|
-
this.
|
|
4461
|
+
this.columnsPerRow - 1,
|
|
4113
4462
|
Math.floor((x - LEFT_GUTTER_WIDTH) / dayWidth),
|
|
4114
4463
|
),
|
|
4115
4464
|
);
|
|
@@ -4123,7 +4472,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4123
4472
|
|
|
4124
4473
|
// Calculate which visual row within the week
|
|
4125
4474
|
const rowInWeek = Math.floor((y - week.yOffset) / this.dayHeight);
|
|
4126
|
-
const dayIndex = rowInWeek * this.
|
|
4475
|
+
const dayIndex = rowInWeek * this.columnsPerRow + col;
|
|
4127
4476
|
|
|
4128
4477
|
if (dayIndex < 0 || dayIndex > 6) return null;
|
|
4129
4478
|
|
|
@@ -4153,9 +4502,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4153
4502
|
convertDateTimeToPosition(date: Date): { x: number; y: number } | null {
|
|
4154
4503
|
if (!this.scrollContainer) return null;
|
|
4155
4504
|
|
|
4156
|
-
const
|
|
4157
|
-
const gridWidth = rect.width - LEFT_GUTTER_WIDTH;
|
|
4158
|
-
const dayWidth = gridWidth / this._columnsPerRow;
|
|
4505
|
+
const dayWidth = this.getDayWidth();
|
|
4159
4506
|
|
|
4160
4507
|
// Find the week that contains this date
|
|
4161
4508
|
const dateStr = date.toDateString();
|
|
@@ -4416,17 +4763,20 @@ export class CalendarViewElement extends LitElement {
|
|
|
4416
4763
|
? this.descriptionSummaryText
|
|
4417
4764
|
: "";
|
|
4418
4765
|
const isSummaryLoading =
|
|
4419
|
-
useDescriptionSummary &&
|
|
4766
|
+
useDescriptionSummary &&
|
|
4767
|
+
hasSummaryStateForCurrentEvent &&
|
|
4768
|
+
this.descriptionSummaryLoading;
|
|
4420
4769
|
const shouldRenderSummary =
|
|
4421
4770
|
useDescriptionSummary && hasSummaryStateForCurrentEvent;
|
|
4422
4771
|
const descriptionToRender = shouldRenderSummary
|
|
4423
|
-
? streamedSummaryText ||
|
|
4772
|
+
? streamedSummaryText ||
|
|
4773
|
+
(isSummaryLoading ? "Generating summary..." : eventDescriptionText)
|
|
4424
4774
|
: eventDescriptionText;
|
|
4425
4775
|
const summarizeButtonLabel = isSummaryLoading
|
|
4426
4776
|
? "Summarizing..."
|
|
4427
4777
|
: shouldRenderSummary
|
|
4428
|
-
|
|
4429
|
-
|
|
4778
|
+
? "Summarize again"
|
|
4779
|
+
: "Summarize";
|
|
4430
4780
|
|
|
4431
4781
|
const formatDate = (date: Date) => {
|
|
4432
4782
|
return new Intl.DateTimeFormat(this.locale, {
|
|
@@ -4573,7 +4923,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
4573
4923
|
|
|
4574
4924
|
<div class="event-detail-body">
|
|
4575
4925
|
${(() => {
|
|
4576
|
-
const teamsMatch = eventDescription?.match(
|
|
4926
|
+
const teamsMatch = eventDescription?.match(
|
|
4927
|
+
/https:\/\/teams\.microsoft\.com\/[^\s<>"]+/,
|
|
4928
|
+
);
|
|
4577
4929
|
const teamsUrl = teamsMatch ? teamsMatch[0] : null;
|
|
4578
4930
|
if (!event.location && !teamsUrl) return null;
|
|
4579
4931
|
return html`
|
|
@@ -4581,7 +4933,11 @@ export class CalendarViewElement extends LitElement {
|
|
|
4581
4933
|
<div class="event-detail-label">Location</div>
|
|
4582
4934
|
<div class="event-detail-value">
|
|
4583
4935
|
${event.location ? html`<div>${event.location}</div>` : null}
|
|
4584
|
-
${
|
|
4936
|
+
${
|
|
4937
|
+
teamsUrl
|
|
4938
|
+
? html`<a href="${teamsUrl}" class="event-detail-link" target="_blank" rel="noopener">Join Microsoft Teams Meeting</a>`
|
|
4939
|
+
: null
|
|
4940
|
+
}
|
|
4585
4941
|
</div>
|
|
4586
4942
|
</div>
|
|
4587
4943
|
`;
|
|
@@ -4677,7 +5033,7 @@ export class CalendarViewElement extends LitElement {
|
|
|
4677
5033
|
|
|
4678
5034
|
${
|
|
4679
5035
|
useDescriptionSummary
|
|
4680
|
-
|
|
5036
|
+
? html`
|
|
4681
5037
|
<div class="event-detail-section">
|
|
4682
5038
|
<div class="event-detail-label">Description</div>
|
|
4683
5039
|
<div class="event-detail-description ${
|
|
@@ -4688,21 +5044,24 @@ export class CalendarViewElement extends LitElement {
|
|
|
4688
5044
|
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
4689
5045
|
</div>
|
|
4690
5046
|
${
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
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
|
+
}
|
|
4695
5051
|
<div class="description-actions">
|
|
4696
5052
|
${
|
|
4697
5053
|
!shouldRenderSummary
|
|
4698
5054
|
? html`<button
|
|
4699
5055
|
class="description-see-more"
|
|
4700
5056
|
@click=${() => {
|
|
4701
|
-
this.isDescriptionExpanded =
|
|
5057
|
+
this.isDescriptionExpanded =
|
|
5058
|
+
!this.isDescriptionExpanded;
|
|
4702
5059
|
this.requestUpdate();
|
|
4703
5060
|
}}
|
|
4704
5061
|
>
|
|
4705
|
-
${
|
|
5062
|
+
${
|
|
5063
|
+
this.isDescriptionExpanded ? "Show less" : "Show more"
|
|
5064
|
+
}
|
|
4706
5065
|
</button>`
|
|
4707
5066
|
: null
|
|
4708
5067
|
}
|
|
@@ -4719,36 +5078,36 @@ export class CalendarViewElement extends LitElement {
|
|
|
4719
5078
|
: event.readOnly ||
|
|
4720
5079
|
(event.organizer != null &&
|
|
4721
5080
|
!this.currentUserEmails.has(event.organizer.email))
|
|
4722
|
-
|
|
5081
|
+
? eventDescription
|
|
4723
5082
|
? html`
|
|
4724
5083
|
<div class="event-detail-section">
|
|
4725
5084
|
<div class="event-detail-label">Description</div>
|
|
4726
5085
|
<div class="event-detail-description ${
|
|
4727
|
-
|
|
4728
|
-
|
|
5086
|
+
this.isDescriptionExpanded ? "expanded" : ""
|
|
5087
|
+
}">
|
|
4729
5088
|
<pre class="event-detail-value">${descriptionToRender}</pre>
|
|
4730
5089
|
</div>
|
|
4731
5090
|
${
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
5091
|
+
eventDescription.length > 300 ||
|
|
5092
|
+
eventDescription.split("\n").length > 8
|
|
5093
|
+
? html`
|
|
4735
5094
|
<button
|
|
4736
5095
|
class="description-see-more"
|
|
4737
5096
|
@click=${() => {
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
5097
|
+
this.isDescriptionExpanded =
|
|
5098
|
+
!this.isDescriptionExpanded;
|
|
5099
|
+
this.requestUpdate();
|
|
5100
|
+
}}
|
|
4742
5101
|
>
|
|
4743
5102
|
${this.isDescriptionExpanded ? "See less" : "See more"}
|
|
4744
5103
|
</button>
|
|
4745
5104
|
`
|
|
4746
|
-
|
|
4747
|
-
|
|
5105
|
+
: null
|
|
5106
|
+
}
|
|
4748
5107
|
</div>
|
|
4749
5108
|
`
|
|
4750
5109
|
: null
|
|
4751
|
-
|
|
5110
|
+
: html`
|
|
4752
5111
|
<div class="event-detail-section">
|
|
4753
5112
|
<div class="event-detail-label">Description</div>
|
|
4754
5113
|
<textarea
|
|
@@ -4872,7 +5231,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
4872
5231
|
@mousedown=${this.onCanvasMouseDown}
|
|
4873
5232
|
@mousemove=${this.onScrollContainerMouseMove}
|
|
4874
5233
|
>
|
|
4875
|
-
<div class="scroll-content" style="height: ${
|
|
5234
|
+
<div class="scroll-content" style="height: ${
|
|
5235
|
+
this.totalHeight
|
|
5236
|
+
}px; width: ${this.getContentWidth()}px;">
|
|
4876
5237
|
</div>
|
|
4877
5238
|
${this.renderSelection()}
|
|
4878
5239
|
</div>
|
|
@@ -4886,27 +5247,42 @@ export class CalendarViewElement extends LitElement {
|
|
|
4886
5247
|
<div class="toolbar">
|
|
4887
5248
|
<div class="toolbar-left">
|
|
4888
5249
|
<button class="toolbar-button" title="Month" @click=${
|
|
4889
|
-
|
|
4890
|
-
|
|
5250
|
+
this.scrollToMonth
|
|
5251
|
+
}>
|
|
4891
5252
|
Month
|
|
4892
5253
|
</button>
|
|
4893
5254
|
<button class="toolbar-button" title="Today" @click=${
|
|
4894
|
-
|
|
4895
|
-
|
|
5255
|
+
this.scrollToToday
|
|
5256
|
+
}>
|
|
4896
5257
|
Today
|
|
4897
5258
|
</button>
|
|
4898
5259
|
<button ?disabled=${
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5260
|
+
this.historyStack.length === 0 ||
|
|
5261
|
+
this.historyIndex >= this.historyStack.length - 1
|
|
5262
|
+
} class="toolbar-button" title="Back" @click=${this.goBack}>
|
|
4902
5263
|
←
|
|
4903
5264
|
</button>
|
|
4904
5265
|
<button ?disabled=${
|
|
4905
|
-
|
|
4906
|
-
|
|
5266
|
+
this.historyStack.length === 0 || this.historyIndex === 0
|
|
5267
|
+
} class="toolbar-button" title="Forward" @click=${
|
|
5268
|
+
this.goForward
|
|
5269
|
+
}>
|
|
4907
5270
|
→
|
|
4908
5271
|
</button>
|
|
4909
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>
|
|
4910
5286
|
<input
|
|
4911
5287
|
type="range"
|
|
4912
5288
|
class="toolbar-zoom-slider"
|
|
@@ -4927,9 +5303,9 @@ export class CalendarViewElement extends LitElement {
|
|
|
4927
5303
|
title="Select theme"
|
|
4928
5304
|
>
|
|
4929
5305
|
${availableThemes.map(
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
5306
|
+
(theme) =>
|
|
5307
|
+
html`<option value="${theme.name}">${theme.label}</option>`,
|
|
5308
|
+
)}
|
|
4933
5309
|
</select>-->
|
|
4934
5310
|
<div class="toolbar-search">
|
|
4935
5311
|
<span class="toolbar-search-icon">🔍</span>
|