@nomideusz/svelte-calendar 0.21.0 → 1.0.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 +502 -269
- package/dist/adapters/composite.d.ts +12 -1
- package/dist/adapters/composite.js +109 -47
- package/dist/adapters/errors.d.ts +22 -0
- package/dist/adapters/errors.js +32 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/jmap.js +26 -17
- package/dist/adapters/mapped.d.ts +2 -2
- package/dist/adapters/mapped.js +18 -9
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/adapters/memory.js +5 -3
- package/dist/adapters/recurring.d.ts +1 -1
- package/dist/adapters/recurring.js +37 -19
- package/dist/adapters/rest.d.ts +2 -2
- package/dist/adapters/rest.js +9 -4
- package/dist/calendar/Calendar.svelte +122 -78
- package/dist/calendar/Calendar.svelte.d.ts +69 -14
- package/dist/calendar/index.d.ts +2 -2
- package/dist/calendar/index.js +1 -1
- package/dist/core/clock.svelte.js +3 -2
- package/dist/core/locale.d.ts +11 -1
- package/dist/core/locale.js +12 -12
- package/dist/core/time.d.ts +9 -0
- package/dist/core/time.js +8 -0
- package/dist/core/timezone.d.ts +7 -3
- package/dist/core/timezone.js +10 -4
- package/dist/engine/event-store.svelte.js +93 -56
- package/dist/engine/view-state.svelte.d.ts +5 -1
- package/dist/engine/view-state.svelte.js +5 -3
- package/dist/headless/create-agenda.svelte.d.ts +8 -1
- package/dist/headless/create-agenda.svelte.js +32 -14
- package/dist/headless/create-calendar.svelte.js +53 -17
- package/dist/headless/create-range-agenda.svelte.d.ts +7 -0
- package/dist/headless/create-range-agenda.svelte.js +33 -12
- package/dist/headless/index.d.ts +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/primitives/DayHeader.svelte +3 -2
- package/dist/primitives/DayHeader.svelte.d.ts +1 -1
- package/dist/primitives/EmptySlot.svelte +5 -2
- package/dist/primitives/EventBlock.svelte +21 -14
- package/dist/primitives/FloatingPanel.svelte +110 -15
- package/dist/primitives/FloatingPanel.svelte.d.ts +7 -5
- package/dist/primitives/NowIndicator.svelte +11 -4
- package/dist/primitives/TimeGutter.svelte +10 -6
- package/dist/primitives/index.d.ts +1 -0
- package/dist/text-fit.d.ts +8 -2
- package/dist/views/agenda/AgendaDay.svelte +41 -33
- package/dist/views/agenda/AgendaWeek.svelte +71 -34
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +2 -0
- package/dist/views/mobile/MobileDay.svelte +98 -41
- package/dist/views/mobile/MobileWeek.svelte +29 -13
- package/dist/views/mobile/swipe.js +6 -1
- package/dist/views/month/MonthGrid.svelte +29 -17
- package/dist/views/month/MonthGrid.svelte.d.ts +1 -1
- package/dist/views/planner/PlannerScroll.svelte +183 -51
- package/dist/views/planner/PlannerWeek.svelte +148 -48
- package/dist/views/shared/context.svelte.d.ts +1 -1
- package/package.json +24 -9
- package/widget/widget.js +7983 -7688
- package/dist/assets/favicon.svg +0 -9
- package/dist/widget/CalendarWidget.svelte +0 -114
- package/dist/widget/CalendarWidget.svelte.d.ts +0 -37
- package/dist/widget/index.d.ts +0 -1
- package/dist/widget/index.js +0 -1
- package/dist/widget/widget.d.ts +0 -10
- package/dist/widget/widget.js +0 -96
|
@@ -20,13 +20,14 @@ import { prefersReducedMotion } from "svelte/motion";
|
|
|
20
20
|
import { useCalendarContext } from "../shared/context.svelte.js";
|
|
21
21
|
import EventContent from "../shared/EventContent.svelte";
|
|
22
22
|
import { createClock } from "../../core/clock.svelte.js";
|
|
23
|
-
import {
|
|
24
|
-
import { startOfWeek as sowFn, isAllDay,
|
|
25
|
-
import { weekdayShort, monthLong, fmtTime as _fmtTime
|
|
23
|
+
import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
|
|
24
|
+
import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
|
|
25
|
+
import { weekdayShort, monthLong, fmtTime as _fmtTime } from "../../core/locale.js";
|
|
26
26
|
import { createChipFit } from "../shared/chip-fit.svelte.js";
|
|
27
|
-
const L = $derived(getLabels());
|
|
28
27
|
let { mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
29
28
|
const ctx = useCalendarContext();
|
|
29
|
+
/** Per-instance labels (Calendar's `labels` prop over the globals). */
|
|
30
|
+
const L = $derived(ctx.labels);
|
|
30
31
|
const clock = createClock(ctx.timezone);
|
|
31
32
|
// Drag ghost flies between day cells instead of teleporting.
|
|
32
33
|
// No fallback: without a counterpart (drag start/end) it appears/disappears instantly.
|
|
@@ -72,6 +73,32 @@ function rowPitch() {
|
|
|
72
73
|
const rows = el?.querySelectorAll("[data-week]");
|
|
73
74
|
return rows && rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop : TIME_H + ROW_MARGIN;
|
|
74
75
|
}
|
|
76
|
+
// ─── Wall-clock helpers ─────────────────────────
|
|
77
|
+
// The axis is wall-clock hours. A DST day is 23 or 25 hours long, so
|
|
78
|
+
// `dayMs + h * HOUR_MS` is an hour off after the change: convert through
|
|
79
|
+
// the local calendar instead.
|
|
80
|
+
/** Fractional wall-clock hour `hour` on the day starting at `dayMs` → epoch ms. */
|
|
81
|
+
function atHour(dayMs, hour) {
|
|
82
|
+
// MakeTime sums the fields in local time, so this may pass 24h.
|
|
83
|
+
return new Date(dayMs).setHours(0, 0, 0, Math.round(hour * HOUR_MS));
|
|
84
|
+
}
|
|
85
|
+
/** Epoch ms → fractional wall-clock hour counted from the day at `dayMs`. */
|
|
86
|
+
function hourOf(ms, dayMs) {
|
|
87
|
+
const d = new Date(ms);
|
|
88
|
+
return diffDays(ms, dayMs) * 24 + d.getHours() + d.getMinutes() / 60 + d.getSeconds() / 3600 + d.getMilliseconds() / HOUR_MS;
|
|
89
|
+
}
|
|
90
|
+
/** All-day events and timed ones a (wall-clock) day or longer are bars;
|
|
91
|
+
* a shorter overnight event is a chip on the day it starts. */
|
|
92
|
+
function inAllDayStrip(ev) {
|
|
93
|
+
return isAllDay(ev) || ev.end.getTime() >= addDaysMs(ev.start.getTime(), 1);
|
|
94
|
+
}
|
|
95
|
+
function statusText(ev) {
|
|
96
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
97
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
98
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
99
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
75
102
|
// ─── Time axis ──────────────────────────────────
|
|
76
103
|
// Without visibleHours the axis spans the hours the loaded events start in:
|
|
77
104
|
// a timetable of 7:00–21:00 classes spread over 24 hours bunches them up.
|
|
@@ -79,7 +106,7 @@ const eventHours = $derived.by(() => {
|
|
|
79
106
|
let lo = 24;
|
|
80
107
|
let hi = -1;
|
|
81
108
|
for (const ev of events) {
|
|
82
|
-
if (
|
|
109
|
+
if (inAllDayStrip(ev)) continue;
|
|
83
110
|
const h = ev.start.getHours() + ev.start.getMinutes() / 60;
|
|
84
111
|
lo = Math.min(lo, h);
|
|
85
112
|
hi = Math.max(hi, h);
|
|
@@ -88,10 +115,10 @@ const eventHours = $derived.by(() => {
|
|
|
88
115
|
});
|
|
89
116
|
const startHour = $derived(visibleHours?.[0] ?? eventHours[0]);
|
|
90
117
|
const endHour = $derived(visibleHours?.[1] ?? eventHours[1]);
|
|
91
|
-
/** Where a chip starting this
|
|
92
|
-
* A chip at the last hour still ends inside the floor height. */
|
|
93
|
-
function timeTop(
|
|
94
|
-
const f = (
|
|
118
|
+
/** Where a chip starting this many wall-clock hours into the day sits, px
|
|
119
|
+
* from the axis top. A chip at the last hour still ends inside the floor height. */
|
|
120
|
+
function timeTop(hoursIntoDay) {
|
|
121
|
+
const f = (hoursIntoDay - startHour) / Math.max(1, endHour - startHour);
|
|
95
122
|
return Math.min(Math.max(f, 0), 1) * (TIME_H - CHIP_H);
|
|
96
123
|
}
|
|
97
124
|
/** Each chip at its start time, or just under the chip before it when that
|
|
@@ -100,7 +127,7 @@ function chipTops(list, dayMs) {
|
|
|
100
127
|
const tops = [];
|
|
101
128
|
let min = 0;
|
|
102
129
|
for (const ev of list) {
|
|
103
|
-
const top = Math.max(timeTop(ev.start.getTime()
|
|
130
|
+
const top = Math.max(timeTop(hourOf(ev.start.getTime(), dayMs)), min);
|
|
104
131
|
tops.push(top);
|
|
105
132
|
min = top + CHIP_H + CHIP_GAP;
|
|
106
133
|
}
|
|
@@ -110,7 +137,7 @@ function chipTops(list, dayMs) {
|
|
|
110
137
|
* it), but never above a chip that has already started — pushed-down
|
|
111
138
|
* chips would read as still to come. */
|
|
112
139
|
function nowTop(list, tops, dayMs) {
|
|
113
|
-
let y = timeTop(clock.tick
|
|
140
|
+
let y = timeTop(hourOf(clock.tick, dayMs));
|
|
114
141
|
list.forEach((ev, i) => {
|
|
115
142
|
if (ev.start.getTime() <= clock.tick) y = Math.max(y, tops[i] + CHIP_H + CHIP_GAP / 2);
|
|
116
143
|
});
|
|
@@ -177,15 +204,17 @@ const weeks = $derived.by(() => {
|
|
|
177
204
|
const isToday = ms === todayMs;
|
|
178
205
|
const isPast = equalDays ? false : ms < todayMs;
|
|
179
206
|
const dayEnd = addDaysMs(ms, 1);
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
// A zero-length event counts where it sits (even at midnight)
|
|
208
|
+
const dayEventsAll = events.filter((ev) => ev.start.getTime() < dayEnd && (ev.end.getTime() > ms || ev.start.getTime() >= ms)).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
209
|
+
// Separate all-day / day-long from timed events. A timed event that
|
|
210
|
+
// runs past midnight (22:00–01:00) is a chip on its start day only.
|
|
182
211
|
const timedEvents = [];
|
|
183
212
|
const allDaySegments = [];
|
|
184
213
|
for (const ev of dayEventsAll) {
|
|
185
|
-
if (
|
|
214
|
+
if (inAllDayStrip(ev)) {
|
|
186
215
|
const seg = segmentForDay(ev, ms);
|
|
187
216
|
if (seg) allDaySegments.push(seg);
|
|
188
|
-
} else {
|
|
217
|
+
} else if (ev.start.getTime() >= ms && ev.end.getTime() >= ev.start.getTime()) {
|
|
189
218
|
timedEvents.push(ev);
|
|
190
219
|
}
|
|
191
220
|
}
|
|
@@ -246,7 +275,15 @@ function fmtAmPm(d) {
|
|
|
246
275
|
// ─── Scroll to current week on mount ────────────────
|
|
247
276
|
onMount(() => {
|
|
248
277
|
tick().then(() => scrollWeekIntoContainer());
|
|
249
|
-
return () =>
|
|
278
|
+
return () => {
|
|
279
|
+
cancelAnimationFrame(syncRaf);
|
|
280
|
+
// Unmounted mid-gesture (a view switch between pointerdown and
|
|
281
|
+
// pointerup): drop the window listeners and the auto-scroll frame,
|
|
282
|
+
// and the half-made drag with them.
|
|
283
|
+
const live = evDragStarted;
|
|
284
|
+
cleanupEvDrag();
|
|
285
|
+
if (live && drag?.active) drag.cancel();
|
|
286
|
+
};
|
|
250
287
|
});
|
|
251
288
|
// Rows grow when their events arrive, which moves every row below them —
|
|
252
289
|
// including the one we centred on an empty grid. Centre once more when the
|
|
@@ -347,31 +384,47 @@ function handleUserScroll() {
|
|
|
347
384
|
}
|
|
348
385
|
}
|
|
349
386
|
}
|
|
350
|
-
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
|
|
354
|
-
|
|
387
|
+
/** The time at a pointer's height on a day's axis — timeTop read backwards,
|
|
388
|
+
* the pointer at the chip's middle. Half hours: the whole day is ~100px.
|
|
389
|
+
* Wall-clock ms into the local day; dayDropStart makes it an instant. */
|
|
390
|
+
function pointerTime(cell, clientY) {
|
|
391
|
+
const area = cell.querySelector(".wg-cell-events");
|
|
392
|
+
if (!area) return startHour * HOUR_MS;
|
|
393
|
+
// The result is wall-clock time into the day (ms), rounded to half hours
|
|
394
|
+
// of the local day — dayDropStart turns it into an instant.
|
|
395
|
+
const y = clientY - area.getBoundingClientRect().top - CHIP_H / 2;
|
|
396
|
+
const f = Math.min(Math.max(y / (TIME_H - CHIP_H), 0), 1);
|
|
397
|
+
const hours = startHour + f * Math.max(1, endHour - startHour);
|
|
398
|
+
return Math.round(hours * 2) * (HOUR_MS / 2);
|
|
399
|
+
}
|
|
400
|
+
/** Where a new thing lands on this day, or null when the day (or that hour
|
|
401
|
+
* of it) refuses it. `at` is the pointer's time into the day; without one
|
|
402
|
+
* (the keyboard) it is the first hour of the axis. One rule, so an
|
|
403
|
+
* empty-cell click and a drop from outside can never disagree. */
|
|
404
|
+
function dayDropStart(ms, at = startHour * HOUR_MS) {
|
|
355
405
|
if (disabledSet.has(ms)) return null;
|
|
356
|
-
const
|
|
406
|
+
const hour = at / HOUR_MS;
|
|
357
407
|
if (blockedSlots?.length) {
|
|
358
408
|
const jsDay = new Date(ms).getDay();
|
|
359
409
|
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
360
410
|
const blocked = blockedSlots.some((slot) => {
|
|
361
411
|
if (slot.day && slot.day !== isoDay) return false;
|
|
362
|
-
return
|
|
412
|
+
return hour >= slot.start && hour < slot.end;
|
|
363
413
|
});
|
|
364
414
|
if (blocked) return null;
|
|
365
415
|
}
|
|
366
|
-
return new Date(ms
|
|
416
|
+
return new Date(atHour(ms, hour));
|
|
367
417
|
}
|
|
368
418
|
function handleDayCellClick(ms, e) {
|
|
369
419
|
const target = e.target;
|
|
370
420
|
if (target.closest(".wg-ev, .wg-ad, .wg-ev-more")) return;
|
|
371
421
|
if (readOnly || !oneventcreate) return;
|
|
372
|
-
const
|
|
422
|
+
const at = e instanceof MouseEvent ? pointerTime(e.currentTarget, e.clientY) : undefined;
|
|
423
|
+
const start = dayDropStart(ms, at);
|
|
373
424
|
if (!start) return;
|
|
374
|
-
|
|
425
|
+
// An hour, or the minimum duration when that is longer. Blocked-slot and
|
|
426
|
+
// max-duration validation is Calendar's (ctx.oneventcreate), not ours.
|
|
427
|
+
const durMin = Math.max(60, minDuration ?? 0);
|
|
375
428
|
oneventcreate({
|
|
376
429
|
start,
|
|
377
430
|
end: new Date(start.getTime() + durMin * 6e4)
|
|
@@ -379,21 +432,31 @@ function handleDayCellClick(ms, e) {
|
|
|
379
432
|
}
|
|
380
433
|
// ─── External drop (HTML5 DnD) ──────────────────────
|
|
381
434
|
// A class chip dragged in from outside the calendar; the cell it is over
|
|
382
|
-
// lights up
|
|
383
|
-
let
|
|
435
|
+
// lights up and a line on its axis says the time it would land at.
|
|
436
|
+
let drop = $state(null);
|
|
384
437
|
function onCellDragOver(e, ms) {
|
|
385
|
-
if (!onexternaldrop || readOnly
|
|
438
|
+
if (!onexternaldrop || readOnly) return;
|
|
439
|
+
const at = pointerTime(e.currentTarget, e.clientY);
|
|
440
|
+
if (!dayDropStart(ms, at)) {
|
|
441
|
+
drop = null;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
386
444
|
e.preventDefault();
|
|
387
445
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
388
|
-
|
|
446
|
+
if (drop?.day !== ms || drop.at !== at) drop = {
|
|
447
|
+
day: ms,
|
|
448
|
+
at
|
|
449
|
+
};
|
|
389
450
|
}
|
|
390
|
-
function onCellDragLeave(ms) {
|
|
391
|
-
|
|
451
|
+
function onCellDragLeave(e, ms) {
|
|
452
|
+
// Leaving for a chip inside the same cell is not leaving the cell.
|
|
453
|
+
if (e.currentTarget.contains(e.relatedTarget)) return;
|
|
454
|
+
if (drop?.day === ms) drop = null;
|
|
392
455
|
}
|
|
393
456
|
function onCellDrop(e, ms) {
|
|
394
|
-
|
|
457
|
+
drop = null;
|
|
395
458
|
if (!onexternaldrop || readOnly || !e.dataTransfer) return;
|
|
396
|
-
const start = dayDropStart(ms);
|
|
459
|
+
const start = dayDropStart(ms, pointerTime(e.currentTarget, e.clientY));
|
|
397
460
|
if (!start) return;
|
|
398
461
|
e.preventDefault();
|
|
399
462
|
onexternaldrop({
|
|
@@ -421,6 +484,11 @@ let evCellW = 100;
|
|
|
421
484
|
* step is the row under the pointer, found by hit-test, not by division.
|
|
422
485
|
* Held as a timestamp: an extension can renumber rows mid-drag. */
|
|
423
486
|
let evStartWeekMs = 0;
|
|
487
|
+
/** The day cell the press landed in; the drag moves the event by the days
|
|
488
|
+
* between it and the cell now under the pointer. */
|
|
489
|
+
let evPressDayMs = 0;
|
|
490
|
+
/** The grid runs right-to-left (`dir="rtl"`): a rightward drag goes back. */
|
|
491
|
+
let evRtl = false;
|
|
424
492
|
const dragPreviewEvent = $derived.by(() => {
|
|
425
493
|
const payload = drag?.active && drag.mode === "move" ? drag.payload : null;
|
|
426
494
|
if (!payload?.eventId) return null;
|
|
@@ -446,15 +514,16 @@ function timedEventsForDay(day) {
|
|
|
446
514
|
const previewKeySnapshot = new Map();
|
|
447
515
|
function dragPreviewTimedForDay(dayMs) {
|
|
448
516
|
const ev = dragPreviewEvent;
|
|
449
|
-
if (!ev ||
|
|
517
|
+
if (!ev || inAllDayStrip(ev)) return null;
|
|
518
|
+
// Where the chip would sit: its start day (as in the week rows)
|
|
450
519
|
const dayEnd = addDaysMs(dayMs, 1);
|
|
451
|
-
const hit = ev.start.getTime()
|
|
520
|
+
const hit = ev.start.getTime() >= dayMs && ev.start.getTime() < dayEnd;
|
|
452
521
|
if (hit) previewKeySnapshot.set("timed", ev.id);
|
|
453
522
|
return hit ? ev : null;
|
|
454
523
|
}
|
|
455
524
|
function dragPreviewSegmentForDay(dayMs) {
|
|
456
525
|
const ev = dragPreviewEvent;
|
|
457
|
-
if (!ev || !
|
|
526
|
+
if (!ev || !inAllDayStrip(ev)) return null;
|
|
458
527
|
const seg = segmentForDay(ev, dayMs);
|
|
459
528
|
if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
|
|
460
529
|
return seg;
|
|
@@ -532,19 +601,45 @@ function onEventPointerDown(e, ev) {
|
|
|
532
601
|
evDragStarted = false;
|
|
533
602
|
evDragId = ev.id;
|
|
534
603
|
evDragEvent = ev;
|
|
604
|
+
const cellMs = Number(e.currentTarget.closest("[data-day]")?.dataset.day);
|
|
605
|
+
evPressDayMs = Number.isFinite(cellMs) ? cellMs : sod(ev.start.getTime());
|
|
535
606
|
window.addEventListener("pointermove", onEvWindowPointerMove);
|
|
536
607
|
window.addEventListener("pointerup", onEvWindowPointerUp, { once: true });
|
|
537
608
|
window.addEventListener("pointercancel", onEvWindowPointerCancel, { once: true });
|
|
538
609
|
}
|
|
610
|
+
/** The day `cols` rendered columns away from the press cell, in the row
|
|
611
|
+
* whose start is `rowMs`. Columns are counted over the days actually shown
|
|
612
|
+
* (hideDays leaves gaps in the dates, not in the columns); running off a
|
|
613
|
+
* row's end carries into the neighbouring row. */
|
|
614
|
+
function dayAtColumn(rowMs, cols) {
|
|
615
|
+
const startRow = weeks.findIndex((w) => w.weekStart === evStartWeekMs);
|
|
616
|
+
let r = weeks.findIndex((w) => w.weekStart === rowMs);
|
|
617
|
+
if (startRow < 0 || r < 0) return null;
|
|
618
|
+
const pressCol = weeks[startRow].days.findIndex((d) => d.ms === evPressDayMs);
|
|
619
|
+
if (pressCol < 0) return null;
|
|
620
|
+
let col = pressCol + cols;
|
|
621
|
+
while (col < 0 && r > 0) {
|
|
622
|
+
r--;
|
|
623
|
+
col += weeks[r].days.length;
|
|
624
|
+
}
|
|
625
|
+
while (col >= weeks[r].days.length && r < weeks.length - 1) {
|
|
626
|
+
col -= weeks[r].days.length;
|
|
627
|
+
r++;
|
|
628
|
+
}
|
|
629
|
+
const days = weeks[r].days;
|
|
630
|
+
if (!days.length) return null;
|
|
631
|
+
return days[Math.max(0, Math.min(days.length - 1, col))].ms;
|
|
632
|
+
}
|
|
539
633
|
/** Where the dragged event sits for the pointer's current offset. */
|
|
540
634
|
function updateDragFromPointer() {
|
|
541
635
|
const ev = evDragEvent;
|
|
542
636
|
if (!drag || !ev) return;
|
|
543
|
-
const
|
|
637
|
+
const dx = (evLastX - evDragStartX) * (evRtl ? -1 : 1);
|
|
638
|
+
const colOffset = Math.round(dx / evCellW);
|
|
544
639
|
const nowWeekMs = weekMsAtY(evLastY);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const deltaDays =
|
|
640
|
+
const target = evStartWeekMs && nowWeekMs ? dayAtColumn(nowWeekMs, colOffset) : null;
|
|
641
|
+
// Without a mapped cell (the rows are mid-rebuild) keep the press day
|
|
642
|
+
const deltaDays = target === null ? 0 : diffDays(target, evPressDayMs);
|
|
548
643
|
drag.updatePointer(new Date(addDaysMs(ev.start.getTime(), deltaDays)), new Date(addDaysMs(ev.end.getTime(), deltaDays)));
|
|
549
644
|
}
|
|
550
645
|
// ─── Auto-scroll under a drag ───────────────────────
|
|
@@ -590,6 +685,7 @@ function onEvWindowPointerMove(e) {
|
|
|
590
685
|
evDragging = true;
|
|
591
686
|
evCellW = getCellWidth();
|
|
592
687
|
evStartWeekMs = weekMsAtY(evDragStartY);
|
|
688
|
+
evRtl = !!el && getComputedStyle(el).direction === "rtl";
|
|
593
689
|
drag.beginMove(ev.id, ev.start, ev.end);
|
|
594
690
|
}
|
|
595
691
|
setAutoScroll(e.clientY);
|
|
@@ -607,6 +703,8 @@ function cleanupEvDrag() {
|
|
|
607
703
|
evAnchor = undefined;
|
|
608
704
|
evDragMovable = false;
|
|
609
705
|
evStartWeekMs = 0;
|
|
706
|
+
evPressDayMs = 0;
|
|
707
|
+
evRtl = false;
|
|
610
708
|
}
|
|
611
709
|
function onEvWindowPointerUp() {
|
|
612
710
|
if (!evDragStarted) {
|
|
@@ -631,9 +729,28 @@ function onWindowKeydown(e) {
|
|
|
631
729
|
// ─── "+N more" per-cell expansion ───────────────────
|
|
632
730
|
let expandedCells = $state({});
|
|
633
731
|
// ─── Roving tabindex for grid cells ─────────────────
|
|
634
|
-
// One tabbable cell (last focused, else today
|
|
732
|
+
// One tabbable cell (last focused, else today, else the first shown day of
|
|
733
|
+
// the anchor week — today may be a hidden day or outside the buffer, and
|
|
734
|
+
// the grid must never drop out of the tab order); arrows move focus.
|
|
635
735
|
let focusedCellMs = $state(null);
|
|
636
|
-
const tabbableCellMs = $derived(
|
|
736
|
+
const tabbableCellMs = $derived.by(() => {
|
|
737
|
+
let fallback = null;
|
|
738
|
+
let anchorFirst = null;
|
|
739
|
+
let focusedShown = false;
|
|
740
|
+
let todayShown = false;
|
|
741
|
+
for (const w of weeks) {
|
|
742
|
+
if (!w.days.length) continue;
|
|
743
|
+
fallback ??= w.days[0].ms;
|
|
744
|
+
if (w.weekStart === anchorPeriodStart) anchorFirst = w.days[0].ms;
|
|
745
|
+
for (const d of w.days) {
|
|
746
|
+
if (d.ms === focusedCellMs) focusedShown = true;
|
|
747
|
+
if (d.ms === todayMs) todayShown = true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (focusedShown) return focusedCellMs;
|
|
751
|
+
if (todayShown) return todayMs;
|
|
752
|
+
return anchorFirst ?? fallback;
|
|
753
|
+
});
|
|
637
754
|
function onCellKeydown(e, ms) {
|
|
638
755
|
if (e.key === "Enter" || e.key === " ") {
|
|
639
756
|
e.preventDefault();
|
|
@@ -730,14 +847,14 @@ function onCellKeydown(e, ms) {
|
|
|
730
847
|
class:wg-cell--weekend={day.isWeekend}
|
|
731
848
|
class:wg-cell--disabled={disabledSet.has(day.ms)}
|
|
732
849
|
class:wg-cell--expanded={isExpanded}
|
|
733
|
-
class:wg-cell--drop={
|
|
850
|
+
class:wg-cell--drop={drop?.day === day.ms}
|
|
734
851
|
role="gridcell"
|
|
735
852
|
data-day={day.ms}
|
|
736
853
|
tabindex={day.ms === tabbableCellMs ? 0 : -1}
|
|
737
854
|
aria-label="{new Date(day.ms).toLocaleDateString(locale ?? 'en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ` (${L.today.toLowerCase()})` : ''}, {L.nEvents(day.events.length + day.allDaySegments.length)}"
|
|
738
855
|
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
739
856
|
ondragover={(e) => onCellDragOver(e, day.ms)}
|
|
740
|
-
ondragleave={() => onCellDragLeave(day.ms)}
|
|
857
|
+
ondragleave={(e) => onCellDragLeave(e, day.ms)}
|
|
741
858
|
ondrop={(e) => onCellDrop(e, day.ms)}
|
|
742
859
|
onfocus={() => { focusedCellMs = day.ms; }}
|
|
743
860
|
onkeydown={(e) => onCellKeydown(e, day.ms)}
|
|
@@ -767,9 +884,9 @@ function onCellKeydown(e, ms) {
|
|
|
767
884
|
{@const isoDay = jsDay === 0 ? 7 : jsDay}
|
|
768
885
|
{#each blockedSlots as slot, i (i)}
|
|
769
886
|
{#if (!slot.day || slot.day === isoDay) && slot.end > startHour && slot.start < endHour}
|
|
770
|
-
{@const slotRange = `${_fmtTime(new Date(day.ms
|
|
771
|
-
{@const bandTop = timeTop(slot.start
|
|
772
|
-
{@const bandH = Math.max(14, timeTop(slot.end
|
|
887
|
+
{@const slotRange = `${_fmtTime(new Date(atHour(day.ms, slot.start)), locale)} – ${_fmtTime(new Date(atHour(day.ms, slot.end)), locale)}`}
|
|
888
|
+
{@const bandTop = timeTop(slot.start)}
|
|
889
|
+
{@const bandH = Math.max(14, timeTop(slot.end) - bandTop)}
|
|
773
890
|
<!-- A chip over the band would cut its label in half: the title keeps it -->
|
|
774
891
|
{@const covered = tops.some((t) => t < bandTop + bandH && t + CHIP_H > bandTop)}
|
|
775
892
|
<div
|
|
@@ -777,7 +894,7 @@ function onCellKeydown(e, ms) {
|
|
|
777
894
|
style:top="{bandTop}px"
|
|
778
895
|
style:height="{bandH}px"
|
|
779
896
|
title="{slot.label ? `${slot.label}, ` : ''}{slotRange}"
|
|
780
|
-
aria-label="{slot.label ||
|
|
897
|
+
aria-label="{slot.label || L.unavailable}, {slotRange}"
|
|
781
898
|
>
|
|
782
899
|
{#if slot.label && !covered}
|
|
783
900
|
<span class="wg-blocked-label">{slot.label}</span>
|
|
@@ -806,7 +923,7 @@ function onCellKeydown(e, ms) {
|
|
|
806
923
|
style:margin-top="{tops[i] - (i ? tops[i - 1] + CHIP_H : 0)}px"
|
|
807
924
|
role="button"
|
|
808
925
|
tabindex="0"
|
|
809
|
-
aria-label="{ev.title}, {fmtAmPm(ev.start)} – {fmtAmPm(ev.end)}{
|
|
926
|
+
aria-label="{ev.title}, {fmtAmPm(ev.start)} – {fmtAmPm(ev.end)}{statusText(ev)}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
810
927
|
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
811
928
|
onpointerenter={() => oneventhover?.(ev)}
|
|
812
929
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
@@ -827,7 +944,7 @@ function onCellKeydown(e, ms) {
|
|
|
827
944
|
<div
|
|
828
945
|
class="wg-ev wg-ev--drag-preview"
|
|
829
946
|
style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
|
|
830
|
-
style:top="{timeTop(previewTimedEvent.start.getTime()
|
|
947
|
+
style:top="{timeTop(hourOf(previewTimedEvent.start.getTime(), day.ms))}px"
|
|
831
948
|
aria-hidden="true"
|
|
832
949
|
in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
833
950
|
out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
|
|
@@ -835,6 +952,11 @@ function onCellKeydown(e, ms) {
|
|
|
835
952
|
{@render timedEventContent(previewTimedEvent, inset)}
|
|
836
953
|
</div>
|
|
837
954
|
{/if}
|
|
955
|
+
{#if drop?.day === day.ms}
|
|
956
|
+
<div class="wg-now wg-drop" style:top="{timeTop(drop.at / HOUR_MS) + CHIP_H / 2}px" aria-hidden="true">
|
|
957
|
+
<span class="wg-now-time">{fmtAmPm(new Date(atHour(day.ms, drop.at / HOUR_MS)))}</span>
|
|
958
|
+
</div>
|
|
959
|
+
{/if}
|
|
838
960
|
{#if nowY !== null}
|
|
839
961
|
<div class="wg-now" style:top="{nowY}px" aria-hidden="true">
|
|
840
962
|
<span class="wg-now-time">{fmtAmPm(new Date(clock.tick))}</span>
|
|
@@ -1230,6 +1352,16 @@ function onCellKeydown(e, ms) {
|
|
|
1230
1352
|
font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
1231
1353
|
color: var(--dt-accent, #2563eb);
|
|
1232
1354
|
}
|
|
1355
|
+
/* Where a dragged-in class would land: the now-line's shape, solid */
|
|
1356
|
+
.wg-drop {
|
|
1357
|
+
z-index: 3;
|
|
1358
|
+
}
|
|
1359
|
+
.wg-drop::after {
|
|
1360
|
+
border-top-style: solid;
|
|
1361
|
+
}
|
|
1362
|
+
.wg-drop .wg-now-time {
|
|
1363
|
+
font-weight: 600;
|
|
1364
|
+
}
|
|
1233
1365
|
|
|
1234
1366
|
.wg-ev {
|
|
1235
1367
|
position: relative; /* above the blocked bands */
|