@nomideusz/svelte-calendar 0.7.5 → 0.9.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/README.md +33 -5
- package/dist/calendar/Calendar.svelte +379 -497
- package/dist/calendar/Calendar.svelte.d.ts +13 -2
- package/dist/core/clock.svelte.d.ts +1 -1
- package/dist/core/clock.svelte.js +20 -9
- package/dist/core/locale.d.ts +4 -0
- package/dist/core/locale.js +4 -0
- package/dist/core/timezone.d.ts +12 -0
- package/dist/core/timezone.js +31 -0
- package/dist/engine/view-state.svelte.d.ts +2 -2
- package/dist/engine/view-state.svelte.js +20 -0
- package/dist/headless/create-calendar.svelte.js +29 -7
- package/dist/headless/types.d.ts +8 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/primitives/DayHeader.svelte +9 -25
- package/dist/primitives/EmptySlot.svelte +15 -31
- package/dist/primitives/EventBlock.svelte +33 -61
- package/dist/primitives/NowIndicator.svelte +16 -42
- package/dist/primitives/TimeGutter.svelte +9 -23
- package/dist/views/agenda/Agenda.svelte +3 -10
- package/dist/views/agenda/AgendaDay.svelte +195 -167
- package/dist/views/agenda/AgendaWeek.svelte +141 -195
- package/dist/views/mobile/Mobile.svelte +3 -10
- package/dist/views/mobile/MobileDay.svelte +422 -266
- package/dist/views/mobile/MobileWeek.svelte +131 -186
- package/dist/views/month/MonthGrid.svelte +334 -0
- package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
- package/dist/views/planner/Planner.svelte +3 -10
- package/dist/views/planner/PlannerDay.svelte +636 -546
- package/dist/views/planner/PlannerWeek.svelte +299 -412
- package/dist/views/shared/EventContent.svelte +16 -0
- package/dist/views/shared/EventContent.svelte.d.ts +9 -0
- package/dist/views/shared/context.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.js +2 -0
- package/dist/widget/CalendarWidget.svelte +79 -116
- package/package.json +3 -2
- package/widget/svelte-calendar.css +321 -10
- package/widget/widget.js +2508 -1056
|
@@ -5,271 +5,344 @@
|
|
|
5
5
|
Large touch targets, swipe left/right to change day.
|
|
6
6
|
Events positioned absolutely within hour lanes.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
8
|
+
<script lang="ts">import { onMount } from "svelte";
|
|
9
|
+
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
10
|
+
import EventContent from "../shared/EventContent.svelte";
|
|
11
|
+
import { createClock } from "../../core/clock.svelte.js";
|
|
12
|
+
import { DAY_MS, HOUR_MS, sod, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
13
|
+
import { fmtH, fmtTime, getLabels } from "../../core/locale.js";
|
|
14
|
+
const L = $derived(getLabels());
|
|
15
|
+
let {
|
|
16
|
+
height = null,
|
|
17
|
+
events = [],
|
|
18
|
+
style = "",
|
|
19
|
+
locale,
|
|
20
|
+
focusDate,
|
|
21
|
+
oneventclick,
|
|
22
|
+
oneventcreate,
|
|
23
|
+
selectedEventId = null,
|
|
24
|
+
readOnly = false,
|
|
25
|
+
visibleHours
|
|
26
|
+
} = $props();
|
|
27
|
+
const ctx = useCalendarContext();
|
|
28
|
+
const viewState = $derived(ctx.viewState);
|
|
29
|
+
const autoHeight = $derived(ctx.autoHeight);
|
|
30
|
+
const oneventhover = $derived(ctx.oneventhover);
|
|
31
|
+
const disabledSet = $derived(ctx.disabledSet);
|
|
32
|
+
const loadRangeCtx = $derived(ctx.loadRange);
|
|
33
|
+
const minDuration = $derived(ctx.minDuration);
|
|
34
|
+
const blockedSlots = $derived(ctx.blockedSlots);
|
|
35
|
+
const drag = $derived(ctx.drag);
|
|
36
|
+
const commitDragCtx = $derived(ctx.commitDrag);
|
|
37
|
+
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
38
|
+
const clock = createClock(ctx.timezone);
|
|
39
|
+
const HOUR_HEIGHT = 64;
|
|
40
|
+
const GUTTER_W = 40;
|
|
41
|
+
const startHour = $derived(visibleHours?.[0] ?? 0);
|
|
42
|
+
const endHour = $derived(visibleHours?.[1] ?? 24);
|
|
43
|
+
const hourCount = $derived(Math.max(1, endHour - startHour));
|
|
44
|
+
const gridHeight = $derived(hourCount * HOUR_HEIGHT);
|
|
45
|
+
const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
46
|
+
const dayEnd = $derived(dayMs + DAY_MS);
|
|
47
|
+
const isToday = $derived(dayMs === clock.today);
|
|
48
|
+
const isPast = $derived(dayMs < clock.today);
|
|
49
|
+
const isDisabled = $derived(disabledSet.has(dayMs));
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (!loadRangeCtx) return;
|
|
52
|
+
const rangeStart = new Date(dayMs - 2 * DAY_MS);
|
|
53
|
+
const rangeEnd = new Date(dayMs + 3 * DAY_MS);
|
|
54
|
+
loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
|
|
55
|
+
return () => loadRangeCtx.set(null);
|
|
56
|
+
});
|
|
57
|
+
const timedEvents = $derived(
|
|
58
|
+
events.filter((ev) => !isAllDay(ev) && !isMultiDay(ev) && ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime())
|
|
59
|
+
);
|
|
60
|
+
const allDayEvents = $derived.by(() => {
|
|
61
|
+
const segs = [];
|
|
62
|
+
for (const ev of events) {
|
|
63
|
+
if (!isAllDay(ev) && !isMultiDay(ev)) continue;
|
|
64
|
+
const seg = segmentForDay(ev, dayMs);
|
|
65
|
+
if (seg) segs.push(seg);
|
|
66
|
+
}
|
|
67
|
+
return segs;
|
|
68
|
+
});
|
|
69
|
+
const positionedEvents = $derived.by(() => {
|
|
70
|
+
const now = clock.tick;
|
|
71
|
+
const sorted = [...timedEvents];
|
|
72
|
+
const rsP = drag?.active && (drag.mode === "resize-start" || drag.mode === "resize-end") ? drag.payload : null;
|
|
73
|
+
let nextEventId = null;
|
|
74
|
+
if (isToday) {
|
|
75
|
+
for (const ev of [...sorted].sort((a, b) => a.start.getTime() - b.start.getTime())) {
|
|
76
|
+
const s = ev.start.getTime();
|
|
77
|
+
if (s > now) {
|
|
78
|
+
nextEventId = ev.id;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const infos = sorted.map((ev) => {
|
|
84
|
+
const resizing = rsP?.eventId === ev.id;
|
|
85
|
+
const evStart = resizing ? rsP.start : ev.start;
|
|
86
|
+
const evEnd = resizing ? rsP.end : ev.end;
|
|
87
|
+
const sMs = Math.max(evStart.getTime(), dayMs + startHour * HOUR_MS);
|
|
88
|
+
const eMs = Math.min(evEnd.getTime(), dayMs + endHour * HOUR_MS);
|
|
89
|
+
const topH = (sMs - dayMs) / HOUR_MS - startHour;
|
|
90
|
+
const botH = (eMs - dayMs) / HOUR_MS - startHour;
|
|
91
|
+
return {
|
|
92
|
+
ev,
|
|
93
|
+
top: topH * HOUR_HEIGHT,
|
|
94
|
+
height: Math.max(24, (botH - topH) * HOUR_HEIGHT),
|
|
95
|
+
isCurrent: ev.start.getTime() <= now && ev.end.getTime() > now,
|
|
96
|
+
isNext: ev.id === nextEventId,
|
|
97
|
+
isResizing: resizing,
|
|
98
|
+
startMs: sMs,
|
|
99
|
+
endMs: eMs,
|
|
100
|
+
col: 0,
|
|
101
|
+
totalCols: 1
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
const par = infos.map((_, i) => i);
|
|
105
|
+
function find(i) {
|
|
106
|
+
while (par[i] !== i) {
|
|
107
|
+
par[i] = par[par[i]];
|
|
108
|
+
i = par[i];
|
|
109
|
+
}
|
|
110
|
+
return i;
|
|
111
|
+
}
|
|
112
|
+
for (let i = 0; i < infos.length; i++) {
|
|
113
|
+
for (let j = i + 1; j < infos.length; j++) {
|
|
114
|
+
if (infos[j].startMs < infos[i].endMs) par[find(i)] = find(j);
|
|
115
|
+
else break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const groups = /* @__PURE__ */ new Map();
|
|
119
|
+
for (let i = 0; i < infos.length; i++) {
|
|
120
|
+
const root = find(i);
|
|
121
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
122
|
+
groups.get(root).push(i);
|
|
123
|
+
}
|
|
124
|
+
for (const [, indices] of groups) {
|
|
125
|
+
const rows = [];
|
|
126
|
+
for (const idx of indices) {
|
|
127
|
+
let row = 0;
|
|
128
|
+
for (let r = 0; r < rows.length; r++) {
|
|
129
|
+
if (rows[r] <= infos[idx].startMs) {
|
|
130
|
+
row = r;
|
|
131
|
+
rows[r] = infos[idx].endMs;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
row = r + 1;
|
|
135
|
+
}
|
|
136
|
+
if (row >= rows.length) rows.push(infos[idx].endMs);
|
|
137
|
+
infos[idx].col = row;
|
|
138
|
+
}
|
|
139
|
+
for (const idx of indices) infos[idx].totalCols = rows.length;
|
|
140
|
+
}
|
|
141
|
+
return infos.map((info) => ({
|
|
142
|
+
ev: info.ev,
|
|
143
|
+
top: info.top,
|
|
144
|
+
height: info.height,
|
|
145
|
+
left: `calc(${GUTTER_W}px + ${info.col / info.totalCols * 100}% - ${GUTTER_W * info.col / info.totalCols}px)`,
|
|
146
|
+
width: `calc(${100 / info.totalCols}% - ${GUTTER_W / info.totalCols + 2}px)`,
|
|
147
|
+
isCurrent: info.isCurrent,
|
|
148
|
+
isNext: info.isNext,
|
|
149
|
+
isResizing: info.isResizing,
|
|
150
|
+
col: info.col,
|
|
151
|
+
totalCols: info.totalCols
|
|
152
|
+
}));
|
|
153
|
+
});
|
|
154
|
+
const nowOffset = $derived.by(() => {
|
|
155
|
+
if (!isToday) return -1;
|
|
156
|
+
const h = (clock.tick - dayMs) / HOUR_MS - startHour;
|
|
157
|
+
if (h < 0 || h > hourCount) return -1;
|
|
158
|
+
return h * HOUR_HEIGHT;
|
|
159
|
+
});
|
|
160
|
+
function isBlockedAt(hour) {
|
|
161
|
+
if (!blockedSlots?.length) return false;
|
|
162
|
+
const jsDay = new Date(dayMs).getDay();
|
|
163
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
164
|
+
return blockedSlots.some((slot) => {
|
|
165
|
+
if (slot.day && slot.day !== isoDay) return false;
|
|
166
|
+
return hour >= slot.start && hour < slot.end;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
let el;
|
|
170
|
+
let touchStartX = 0;
|
|
171
|
+
let touchStartY = 0;
|
|
172
|
+
let swiping = false;
|
|
173
|
+
let swipeOffset = $state(0);
|
|
174
|
+
const SWIPE_THRESHOLD = 50;
|
|
175
|
+
function onTouchStart(e) {
|
|
176
|
+
const t = e.touches[0];
|
|
177
|
+
touchStartX = t.clientX;
|
|
178
|
+
touchStartY = t.clientY;
|
|
179
|
+
swiping = true;
|
|
180
|
+
swipeOffset = 0;
|
|
181
|
+
}
|
|
182
|
+
function onTouchMove(e) {
|
|
183
|
+
if (!swiping) return;
|
|
184
|
+
const t = e.touches[0];
|
|
185
|
+
const dx = t.clientX - touchStartX;
|
|
186
|
+
const dy = t.clientY - touchStartY;
|
|
187
|
+
if (Math.abs(dy) > Math.abs(dx) * 0.8) {
|
|
188
|
+
swiping = false;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
swipeOffset = dx;
|
|
192
|
+
}
|
|
193
|
+
function onTouchEnd() {
|
|
194
|
+
if (!swiping) {
|
|
195
|
+
swipeOffset = 0;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (Math.abs(swipeOffset) > SWIPE_THRESHOLD) {
|
|
199
|
+
if (swipeOffset > 0) {
|
|
200
|
+
viewState?.prev();
|
|
201
|
+
} else {
|
|
202
|
+
viewState?.next();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
swipeOffset = 0;
|
|
206
|
+
swiping = false;
|
|
207
|
+
}
|
|
208
|
+
function handleGridClick(e) {
|
|
209
|
+
if (suppressGridClick) {
|
|
210
|
+
suppressGridClick = false;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!oneventcreate || readOnly || isDisabled) return;
|
|
214
|
+
if (e.target.closest(".mb-event")) return;
|
|
215
|
+
const grid = e.currentTarget;
|
|
216
|
+
const rect = grid.getBoundingClientRect();
|
|
217
|
+
const y = e.clientY - rect.top + grid.scrollTop;
|
|
218
|
+
const hour = startHour + y / HOUR_HEIGHT;
|
|
219
|
+
if (isBlockedAt(hour)) return;
|
|
220
|
+
const snapHour = Math.floor(hour);
|
|
221
|
+
const durMin = minDuration ? Math.max(60, minDuration) : 60;
|
|
222
|
+
const start = new Date(dayMs + snapHour * HOUR_MS);
|
|
223
|
+
const end = new Date(start.getTime() + durMin * 6e4);
|
|
224
|
+
oneventcreate({ start, end });
|
|
225
|
+
}
|
|
226
|
+
const CREATE_THRESHOLD = 4;
|
|
227
|
+
let suppressGridClick = false;
|
|
228
|
+
let mbCreateStartY = 0;
|
|
229
|
+
let mbCreateAnchorMs = 0;
|
|
230
|
+
let mbCreateStarted = false;
|
|
231
|
+
function gridTimeMs(clientY) {
|
|
232
|
+
const rect = gridEl.getBoundingClientRect();
|
|
233
|
+
const y = clientY - rect.top + gridEl.scrollTop;
|
|
234
|
+
return dayMs + (startHour + y / HOUR_HEIGHT) * HOUR_MS;
|
|
235
|
+
}
|
|
236
|
+
function clampToDay(ms) {
|
|
237
|
+
return Math.max(dayMs + startHour * HOUR_MS, Math.min(dayMs + endHour * HOUR_MS, ms));
|
|
238
|
+
}
|
|
239
|
+
function onGridPointerDown(e) {
|
|
240
|
+
if (e.button !== 0 || !drag || !oneventcreate || readOnly || isDisabled) return;
|
|
241
|
+
if (e.target.closest(".mb-event")) return;
|
|
242
|
+
mbCreateStartY = e.clientY;
|
|
243
|
+
mbCreateAnchorMs = gridTimeMs(e.clientY);
|
|
244
|
+
mbCreateStarted = false;
|
|
245
|
+
window.addEventListener("pointermove", onGridCreateMove);
|
|
246
|
+
window.addEventListener("pointerup", onGridCreateUp, { once: true });
|
|
247
|
+
window.addEventListener("pointercancel", onGridCreateCancel, { once: true });
|
|
248
|
+
}
|
|
249
|
+
function onGridCreateMove(e) {
|
|
250
|
+
if (!drag) return;
|
|
251
|
+
if (!mbCreateStarted) {
|
|
252
|
+
if (Math.abs(e.clientY - mbCreateStartY) < CREATE_THRESHOLD) return;
|
|
253
|
+
mbCreateStarted = true;
|
|
254
|
+
mbCreateAnchorMs = clampToDay(Math.floor(mbCreateAnchorMs / SNAP_MS) * SNAP_MS);
|
|
255
|
+
drag.beginCreate(new Date(mbCreateAnchorMs), new Date(mbCreateAnchorMs + SNAP_MS));
|
|
256
|
+
}
|
|
257
|
+
const snapped = clampToDay(Math.round(gridTimeMs(e.clientY) / SNAP_MS) * SNAP_MS);
|
|
258
|
+
drag.updatePointer(
|
|
259
|
+
new Date(Math.min(mbCreateAnchorMs, snapped)),
|
|
260
|
+
new Date(Math.max(mbCreateAnchorMs + SNAP_MS, snapped))
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
function cleanupGridCreate() {
|
|
264
|
+
window.removeEventListener("pointermove", onGridCreateMove);
|
|
265
|
+
window.removeEventListener("pointerup", onGridCreateUp);
|
|
266
|
+
window.removeEventListener("pointercancel", onGridCreateCancel);
|
|
267
|
+
mbCreateStarted = false;
|
|
268
|
+
}
|
|
269
|
+
function onGridCreateUp() {
|
|
270
|
+
if (drag && mbCreateStarted) {
|
|
271
|
+
suppressGridClick = true;
|
|
272
|
+
commitDragCtx?.();
|
|
273
|
+
setTimeout(() => {
|
|
274
|
+
suppressGridClick = false;
|
|
275
|
+
}, 0);
|
|
276
|
+
}
|
|
277
|
+
cleanupGridCreate();
|
|
278
|
+
}
|
|
279
|
+
function onGridCreateCancel() {
|
|
280
|
+
if (drag && mbCreateStarted) drag.cancel();
|
|
281
|
+
cleanupGridCreate();
|
|
282
|
+
}
|
|
283
|
+
let suppressEventClick = false;
|
|
284
|
+
let mbRsStartY = 0;
|
|
285
|
+
let mbRsStarted = false;
|
|
286
|
+
let mbRsEdge = "end";
|
|
287
|
+
let mbRsEvent = null;
|
|
288
|
+
function onResizePointerDown(e, ev, edge) {
|
|
289
|
+
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
mbRsStartY = e.clientY;
|
|
292
|
+
mbRsStarted = false;
|
|
293
|
+
mbRsEdge = edge;
|
|
294
|
+
mbRsEvent = ev;
|
|
295
|
+
window.addEventListener("pointermove", onResizeMove);
|
|
296
|
+
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
297
|
+
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
298
|
+
}
|
|
299
|
+
function onResizeMove(e) {
|
|
300
|
+
const ev = mbRsEvent;
|
|
301
|
+
if (!drag || !ev) return;
|
|
302
|
+
if (!mbRsStarted) {
|
|
303
|
+
if (Math.abs(e.clientY - mbRsStartY) < CREATE_THRESHOLD) return;
|
|
304
|
+
mbRsStarted = true;
|
|
305
|
+
drag.beginResize(ev.id, mbRsEdge, ev.start, ev.end);
|
|
306
|
+
}
|
|
307
|
+
const snapped = clampToDay(Math.round(gridTimeMs(e.clientY) / SNAP_MS) * SNAP_MS);
|
|
308
|
+
if (mbRsEdge === "end") {
|
|
309
|
+
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
310
|
+
drag.updatePointer(ev.start, new Date(end));
|
|
311
|
+
} else {
|
|
312
|
+
const start = Math.min(snapped, ev.end.getTime() - SNAP_MS);
|
|
313
|
+
drag.updatePointer(new Date(start), ev.end);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function cleanupResize() {
|
|
317
|
+
window.removeEventListener("pointermove", onResizeMove);
|
|
318
|
+
window.removeEventListener("pointerup", onResizeUp);
|
|
319
|
+
window.removeEventListener("pointercancel", onResizeCancel);
|
|
320
|
+
mbRsStarted = false;
|
|
321
|
+
mbRsEvent = null;
|
|
322
|
+
}
|
|
323
|
+
function onResizeUp() {
|
|
324
|
+
if (drag && mbRsStarted) {
|
|
325
|
+
suppressEventClick = true;
|
|
326
|
+
suppressGridClick = true;
|
|
327
|
+
commitDragCtx?.();
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
suppressEventClick = false;
|
|
330
|
+
suppressGridClick = false;
|
|
331
|
+
}, 0);
|
|
332
|
+
}
|
|
333
|
+
cleanupResize();
|
|
334
|
+
}
|
|
335
|
+
function onResizeCancel() {
|
|
336
|
+
if (drag && mbRsStarted) drag.cancel();
|
|
337
|
+
cleanupResize();
|
|
338
|
+
}
|
|
339
|
+
let gridEl;
|
|
340
|
+
onMount(() => {
|
|
341
|
+
if (nowOffset > 0 && gridEl) {
|
|
342
|
+
const scrollTarget = Math.max(0, nowOffset - 120);
|
|
343
|
+
gridEl.scrollTop = scrollTarget;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
273
346
|
</script>
|
|
274
347
|
|
|
275
348
|
<div
|
|
@@ -311,6 +384,7 @@
|
|
|
311
384
|
class="mb-grid"
|
|
312
385
|
bind:this={gridEl}
|
|
313
386
|
onclick={handleGridClick}
|
|
387
|
+
onpointerdown={onGridPointerDown}
|
|
314
388
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleGridClick(e as unknown as MouseEvent); }}
|
|
315
389
|
role="grid"
|
|
316
390
|
tabindex="-1"
|
|
@@ -356,17 +430,19 @@
|
|
|
356
430
|
class:mb-event--tentative={p.ev.status === 'tentative'}
|
|
357
431
|
class:mb-event--full={p.ev.status === 'full'}
|
|
358
432
|
class:mb-event--limited={p.ev.status === 'limited'}
|
|
433
|
+
class:mb-event--resizing={p.isResizing}
|
|
359
434
|
style:top="{p.top}px"
|
|
360
435
|
style:height="{p.height}px"
|
|
361
436
|
style:left={p.left}
|
|
362
437
|
style:width={p.width}
|
|
363
438
|
style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
|
|
364
|
-
onclick={(e) => { e.stopPropagation(); oneventclick?.(p.ev); }}
|
|
439
|
+
onclick={(e) => { e.stopPropagation(); if (suppressEventClick) { suppressEventClick = false; return; } oneventclick?.(p.ev); }}
|
|
365
440
|
onpointerenter={() => oneventhover?.(p.ev)}
|
|
366
441
|
aria-label="{p.ev.title}{p.ev.status === 'cancelled' ? ' (cancelled)' : ''}{p.ev.status === 'tentative' ? ' (tentative)' : ''}{p.ev.status === 'full' ? ' (full)' : ''}{p.ev.status === 'limited' ? ' (limited)' : ''}{p.isCurrent ? `, ${L.inProgress}` : ''}{p.isNext ? `, ${L.upNext}` : ''}"
|
|
367
442
|
>
|
|
368
443
|
<div class="mb-ev-stripe"></div>
|
|
369
444
|
<div class="mb-ev-body">
|
|
445
|
+
<EventContent event={p.ev}>
|
|
370
446
|
<span class="mb-ev-title">{p.ev.title}</span>
|
|
371
447
|
{#if p.height > 32}
|
|
372
448
|
<span class="mb-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
@@ -384,14 +460,38 @@
|
|
|
384
460
|
{/each}
|
|
385
461
|
</div>
|
|
386
462
|
{/if}
|
|
463
|
+
</EventContent>
|
|
387
464
|
</div>
|
|
388
465
|
{#if p.isCurrent}
|
|
389
466
|
<span class="mb-ev-live"></span>
|
|
390
467
|
{:else if p.isNext}
|
|
391
468
|
<span class="mb-ev-next-badge">{L.upNext}</span>
|
|
392
469
|
{/if}
|
|
470
|
+
{#if !readOnly && !p.ev.data?.readOnly}
|
|
471
|
+
<span
|
|
472
|
+
class="mb-ev-handle mb-ev-handle--start"
|
|
473
|
+
aria-hidden="true"
|
|
474
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'start')}
|
|
475
|
+
></span>
|
|
476
|
+
<span
|
|
477
|
+
class="mb-ev-handle mb-ev-handle--end"
|
|
478
|
+
aria-hidden="true"
|
|
479
|
+
onpointerdown={(e) => onResizePointerDown(e, p.ev, 'end')}
|
|
480
|
+
></span>
|
|
481
|
+
{/if}
|
|
393
482
|
</button>
|
|
394
483
|
{/each}
|
|
484
|
+
|
|
485
|
+
<!-- Drag-to-create ghost -->
|
|
486
|
+
{#if !readOnly && drag?.active && drag.mode === 'create' && drag.payload}
|
|
487
|
+
{@const gTop = ((drag.payload.start.getTime() - dayMs) / HOUR_MS - startHour) * HOUR_HEIGHT}
|
|
488
|
+
{@const gH = Math.max(12, ((drag.payload.end.getTime() - drag.payload.start.getTime()) / HOUR_MS) * HOUR_HEIGHT)}
|
|
489
|
+
<div class="mb-create-ghost" style:top="{gTop}px" style:height="{gH}px" aria-hidden="true">
|
|
490
|
+
<span class="mb-create-ghost-time">
|
|
491
|
+
{fmtTime(drag.payload.start, locale)} – {fmtTime(drag.payload.end, locale)}
|
|
492
|
+
</span>
|
|
493
|
+
</div>
|
|
494
|
+
{/if}
|
|
395
495
|
</div>
|
|
396
496
|
</div>
|
|
397
497
|
</div>
|
|
@@ -627,6 +727,62 @@
|
|
|
627
727
|
opacity: 0.65;
|
|
628
728
|
border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
|
|
629
729
|
}
|
|
730
|
+
.mb-event--resizing {
|
|
731
|
+
z-index: 50;
|
|
732
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
733
|
+
cursor: ns-resize;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/* ─── Resize handles ─────────────────────────────── */
|
|
737
|
+
.mb-ev-handle {
|
|
738
|
+
position: absolute;
|
|
739
|
+
left: 0;
|
|
740
|
+
right: 0;
|
|
741
|
+
height: 10px;
|
|
742
|
+
z-index: 2;
|
|
743
|
+
cursor: ns-resize;
|
|
744
|
+
touch-action: none;
|
|
745
|
+
}
|
|
746
|
+
.mb-ev-handle--start { top: 0; }
|
|
747
|
+
.mb-ev-handle--end { bottom: 0; }
|
|
748
|
+
.mb-ev-handle::after {
|
|
749
|
+
content: '';
|
|
750
|
+
position: absolute;
|
|
751
|
+
left: 50%;
|
|
752
|
+
transform: translateX(-50%);
|
|
753
|
+
width: 24px;
|
|
754
|
+
height: 3px;
|
|
755
|
+
border-radius: 2px;
|
|
756
|
+
background: var(--ev-color);
|
|
757
|
+
opacity: 0;
|
|
758
|
+
transition: opacity 120ms;
|
|
759
|
+
}
|
|
760
|
+
.mb-ev-handle--start::after { top: 2px; }
|
|
761
|
+
.mb-ev-handle--end::after { bottom: 2px; }
|
|
762
|
+
.mb-event:hover .mb-ev-handle::after,
|
|
763
|
+
.mb-event--resizing .mb-ev-handle::after,
|
|
764
|
+
.mb-event--selected .mb-ev-handle::after { opacity: 0.55; }
|
|
765
|
+
|
|
766
|
+
/* ─── Drag-to-create ghost ───────────────────────── */
|
|
767
|
+
.mb-create-ghost {
|
|
768
|
+
position: absolute;
|
|
769
|
+
left: 40px;
|
|
770
|
+
right: 4px;
|
|
771
|
+
z-index: 40;
|
|
772
|
+
border-radius: 8px;
|
|
773
|
+
background: color-mix(in srgb, var(--dt-accent, #2563eb) 12%, transparent);
|
|
774
|
+
border: 1px dashed color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
|
|
775
|
+
display: flex;
|
|
776
|
+
align-items: flex-start;
|
|
777
|
+
overflow: hidden;
|
|
778
|
+
pointer-events: none;
|
|
779
|
+
}
|
|
780
|
+
.mb-create-ghost-time {
|
|
781
|
+
font: 600 10px/1 var(--dt-mono, ui-monospace, monospace);
|
|
782
|
+
color: var(--dt-accent, #2563eb);
|
|
783
|
+
padding: 4px 8px;
|
|
784
|
+
white-space: nowrap;
|
|
785
|
+
}
|
|
630
786
|
|
|
631
787
|
.mb-ev-stripe {
|
|
632
788
|
width: 4px;
|