@nomideusz/svelte-calendar 0.14.1 → 0.15.2
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/Calendar.svelte +450 -372
- package/dist/calendar/Calendar.svelte.d.ts +2 -1
- package/dist/headless/types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/primitives/DayHeader.svelte +1 -7
- package/dist/primitives/EmptySlot.svelte +8 -10
- package/dist/primitives/EventBlock.svelte +12 -22
- package/dist/primitives/FloatingPanel.svelte +191 -0
- package/dist/primitives/FloatingPanel.svelte.d.ts +23 -0
- package/dist/primitives/NowIndicator.svelte +2 -13
- package/dist/primitives/TimeGutter.svelte +1 -7
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/views/agenda/AgendaDay.svelte +68 -45
- package/dist/views/agenda/AgendaWeek.svelte +120 -105
- package/dist/views/mobile/MobileDay.svelte +313 -276
- package/dist/views/mobile/MobileWeek.svelte +91 -84
- package/dist/views/month/MonthGrid.svelte +101 -104
- package/dist/views/planner/PlannerWeek.svelte +475 -407
- package/dist/views/planner/PlannerWeek.svelte.d.ts +1 -1
- package/dist/widget/CalendarWidget.svelte +64 -83
- package/package.json +17 -10
- package/widget/widget.js +12723 -12203
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
• Timed events are absolutely positioned by start/end; overlaps share the
|
|
9
9
|
column via union-find lanes.
|
|
10
10
|
• Drag-to-move (vertical = time, horizontal = day), top/bottom resize
|
|
11
|
-
|
|
11
|
+
grips (hit zone limited to the centered grip so short events stay
|
|
12
|
+
grab-to-move), drag-to-create with ghost preview — all through ctx.drag /
|
|
12
13
|
ctx.commitDrag so Calendar's validation (min/max duration, blocked,
|
|
13
14
|
disabled) applies on drop.
|
|
14
15
|
• Now-line across today's column only; auto-scrolls to the current time.
|
|
@@ -20,20 +21,8 @@ import { createClock } from "../../core/clock.svelte.js";
|
|
|
20
21
|
import { DAY_MS, HOUR_MS, sod, addDaysMs } from "../../core/time.js";
|
|
21
22
|
import { startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
22
23
|
import { fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong } from "../../core/locale.js";
|
|
23
|
-
let {
|
|
24
|
-
|
|
25
|
-
mondayStart = true,
|
|
26
|
-
locale,
|
|
27
|
-
height = 520,
|
|
28
|
-
events = [],
|
|
29
|
-
style = "",
|
|
30
|
-
focusDate,
|
|
31
|
-
oneventclick,
|
|
32
|
-
oneventcreate,
|
|
33
|
-
selectedEventId = null,
|
|
34
|
-
readOnly = false,
|
|
35
|
-
visibleHours
|
|
36
|
-
} = $props();
|
|
24
|
+
let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
25
|
+
// ── Context ────────────────────────────────────────
|
|
37
26
|
const ctx = useCalendarContext();
|
|
38
27
|
const L = $derived(ctx.labels);
|
|
39
28
|
const clock = createClock(ctx.timezone);
|
|
@@ -51,6 +40,7 @@ const autoHeight = $derived(ctx.autoHeight);
|
|
|
51
40
|
const oneventhover = $derived(ctx.oneventhover);
|
|
52
41
|
const disabledSet = $derived(ctx.disabledSet);
|
|
53
42
|
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
43
|
+
// ── Config ─────────────────────────────────────────
|
|
54
44
|
const HOUR_H = 48;
|
|
55
45
|
const GUTTER_W = 48;
|
|
56
46
|
const MIN_COL_W = $derived(ctx.minColumnWidth);
|
|
@@ -61,261 +51,313 @@ const hourCount = $derived(Math.max(1, endHour - startHour));
|
|
|
61
51
|
const gridHeight = $derived(hourCount * HOUR_H);
|
|
62
52
|
let scrollEl;
|
|
63
53
|
let colsEl;
|
|
54
|
+
// ── Week window ────────────────────────────────────
|
|
55
|
+
// The engine (viewState.range) already computes the week window; fall back
|
|
56
|
+
// to a local computation when running headless.
|
|
64
57
|
const todayMs = $derived(clock.today);
|
|
58
|
+
// Day mode is the same grid at one column (viewState.mode covers in-Calendar
|
|
59
|
+
// use where the view id decides; the prop covers headless/direct use).
|
|
65
60
|
const singleDay = $derived(mode === "day" || viewState?.mode === "day");
|
|
66
61
|
const customDays = $derived(singleDay ? 1 : viewState?.dayCount ?? 7);
|
|
62
|
+
// Calendar's chrome date label renders exactly when showDates is on, and in
|
|
63
|
+
// day mode it already names the visible day — drop the column header so the
|
|
64
|
+
// date never appears twice. Headless or showDates=false hosts keep it; a
|
|
65
|
+
// custom dayHeader snippet always renders.
|
|
67
66
|
const hideDayHead = $derived(singleDay && showDates && !!viewState && !dayHeaderSnippet);
|
|
68
67
|
const weekStartMs = $derived.by(() => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const r = viewState?.range;
|
|
69
|
+
if (r) return sod(r.start.getTime());
|
|
70
|
+
const f = focusDate?.getTime() ?? todayMs;
|
|
71
|
+
return customDays === 7 ? sowFn(f, mondayStart) : sod(f);
|
|
73
72
|
});
|
|
74
73
|
const weekEndMs = $derived(addDaysMs(weekStartMs, customDays));
|
|
75
74
|
const weekHasToday = $derived(todayMs >= weekStartMs && todayMs < weekEndMs);
|
|
76
75
|
const dayCols = $derived.by(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
76
|
+
const cols = [];
|
|
77
|
+
for (let d = 0; d < customDays; d++) {
|
|
78
|
+
const ms = addDaysMs(weekStartMs, d);
|
|
79
|
+
const date = new Date(ms);
|
|
80
|
+
const dow = date.getDay();
|
|
81
|
+
const isoDay = dow === 0 ? 7 : dow;
|
|
82
|
+
if (hideDays?.includes(isoDay)) continue;
|
|
83
|
+
cols.push({
|
|
84
|
+
ms,
|
|
85
|
+
isToday: ms === todayMs,
|
|
86
|
+
isPast: equalDays ? false : ms < todayMs,
|
|
87
|
+
isWeekend: dow === 0 || dow === 6,
|
|
88
|
+
isDisabled: disabledSet.has(ms),
|
|
89
|
+
isoDay,
|
|
90
|
+
dayNum: date.getDate()
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return cols;
|
|
95
94
|
});
|
|
96
95
|
const innerMinWidth = $derived(GUTTER_W + dayCols.length * MIN_COL_W);
|
|
96
|
+
// ── Load range: visible week ±7 days ───────────────
|
|
97
97
|
$effect(() => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
if (!loadRangeCtx) return;
|
|
99
|
+
const rangeStart = new Date(weekStartMs - 7 * DAY_MS);
|
|
100
|
+
const rangeEnd = new Date(weekEndMs + 7 * DAY_MS);
|
|
101
|
+
loadRangeCtx.set({
|
|
102
|
+
start: rangeStart,
|
|
103
|
+
end: rangeEnd
|
|
104
|
+
});
|
|
105
|
+
return () => loadRangeCtx.set(null);
|
|
103
106
|
});
|
|
107
|
+
// ── All-day / multi-day segments per day ───────────
|
|
104
108
|
const allDayByDay = $derived.by(() => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
const map = new Map();
|
|
110
|
+
for (const day of dayCols) {
|
|
111
|
+
const segs = [];
|
|
112
|
+
for (const ev of events) {
|
|
113
|
+
if (!isAllDay(ev) && !isMultiDay(ev)) continue;
|
|
114
|
+
const seg = segmentForDay(ev, day.ms);
|
|
115
|
+
if (seg) segs.push(seg);
|
|
116
|
+
}
|
|
117
|
+
if (segs.length) map.set(day.ms, segs);
|
|
118
|
+
}
|
|
119
|
+
return map;
|
|
116
120
|
});
|
|
117
121
|
const hasAllDayRow = $derived(allDayByDay.size > 0);
|
|
118
122
|
let adExpanded = $state({});
|
|
123
|
+
// ── Timed event layout (union-find lanes, vertical) ─
|
|
124
|
+
// The dragged (move-mode) event keeps its lane at the original time, dimmed,
|
|
125
|
+
// while the ghost tracks the pointer; resize tracks the tentative payload
|
|
126
|
+
// in place.
|
|
119
127
|
const movingId = $derived(drag?.active && drag.mode === "move" ? drag.payload?.eventId ?? null : null);
|
|
120
128
|
const movingEvent = $derived(movingId ? events.find((e) => e.id === movingId) ?? null : null);
|
|
121
129
|
const layoutByDay = $derived.by(() => {
|
|
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
|
-
|
|
130
|
+
const rsP = drag?.active && (drag.mode === "resize-start" || drag.mode === "resize-end") ? drag.payload : null;
|
|
131
|
+
const map = new Map();
|
|
132
|
+
for (const day of dayCols) {
|
|
133
|
+
const dayEnd = day.ms + DAY_MS;
|
|
134
|
+
const bandStart = day.ms + startHour * HOUR_MS;
|
|
135
|
+
const bandEnd = day.ms + endHour * HOUR_MS;
|
|
136
|
+
const infos = [];
|
|
137
|
+
for (const ev of events) {
|
|
138
|
+
if (isAllDay(ev) || isMultiDay(ev)) continue;
|
|
139
|
+
// The event being moved stays laned at its ORIGINAL slot, dimmed:
|
|
140
|
+
// the ghost shows where it is going, the faded block where it came
|
|
141
|
+
// from, and neither the drop target nor its neighbours re-lane
|
|
142
|
+
// under the pointer mid-drag.
|
|
143
|
+
const isResizing = rsP?.eventId === ev.id;
|
|
144
|
+
const s0 = isResizing ? rsP.start.getTime() : ev.start.getTime();
|
|
145
|
+
const e0 = isResizing ? rsP.end.getTime() : ev.end.getTime();
|
|
146
|
+
if (s0 >= dayEnd || e0 <= day.ms) continue;
|
|
147
|
+
const sMs = Math.max(s0, bandStart);
|
|
148
|
+
const eMs = Math.min(e0, bandEnd);
|
|
149
|
+
// Entirely outside the visible hour band — skip, don't paint a sliver
|
|
150
|
+
if (eMs <= sMs) continue;
|
|
151
|
+
infos.push({
|
|
152
|
+
ev,
|
|
153
|
+
startMs: sMs,
|
|
154
|
+
endMs: eMs,
|
|
155
|
+
isResizing,
|
|
156
|
+
isMoving: ev.id === movingId,
|
|
157
|
+
col: 0,
|
|
158
|
+
totalCols: 1
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
infos.sort((a, b) => a.startMs - b.startMs || b.endMs - a.endMs);
|
|
162
|
+
// Union-find overlap groups (same algorithm as MobileDay)
|
|
163
|
+
const par = infos.map((_, i) => i);
|
|
164
|
+
function find(i) {
|
|
165
|
+
while (par[i] !== i) {
|
|
166
|
+
par[i] = par[par[i]];
|
|
167
|
+
i = par[i];
|
|
168
|
+
}
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
for (let i = 0; i < infos.length; i++) {
|
|
172
|
+
for (let j = i + 1; j < infos.length; j++) {
|
|
173
|
+
if (infos[j].startMs < infos[i].endMs) par[find(i)] = find(j);
|
|
174
|
+
else break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const groups = new Map();
|
|
178
|
+
for (let i = 0; i < infos.length; i++) {
|
|
179
|
+
const root = find(i);
|
|
180
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
181
|
+
groups.get(root).push(i);
|
|
182
|
+
}
|
|
183
|
+
for (const [, indices] of groups) {
|
|
184
|
+
const lanes = [];
|
|
185
|
+
for (const idx of indices) {
|
|
186
|
+
const inf = infos[idx];
|
|
187
|
+
let lane = 0;
|
|
188
|
+
for (let r = 0; r < lanes.length; r++) {
|
|
189
|
+
if (lanes[r] <= inf.startMs) {
|
|
190
|
+
lane = r;
|
|
191
|
+
lanes[r] = inf.endMs;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
lane = r + 1;
|
|
195
|
+
}
|
|
196
|
+
if (lane >= lanes.length) lanes.push(inf.endMs);
|
|
197
|
+
infos[idx].col = lane;
|
|
198
|
+
}
|
|
199
|
+
for (const idx of indices) infos[idx].totalCols = lanes.length;
|
|
200
|
+
}
|
|
201
|
+
map.set(day.ms, infos.map((inf) => ({
|
|
202
|
+
ev: inf.ev,
|
|
203
|
+
top: ((inf.startMs - day.ms) / HOUR_MS - startHour) * HOUR_H,
|
|
204
|
+
height: Math.max(24, (inf.endMs - inf.startMs) / HOUR_MS * HOUR_H),
|
|
205
|
+
col: inf.col,
|
|
206
|
+
totalCols: inf.totalCols,
|
|
207
|
+
isResizing: inf.isResizing,
|
|
208
|
+
isMoving: inf.isMoving
|
|
209
|
+
})));
|
|
210
|
+
}
|
|
211
|
+
return map;
|
|
193
212
|
});
|
|
213
|
+
// ── Now markers (separate from layout — 1Hz tick must not re-lane) ──
|
|
194
214
|
const nowIds = $derived.by(() => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
const now = clock.tick;
|
|
216
|
+
const s = new Set();
|
|
217
|
+
for (const ev of events) {
|
|
218
|
+
if (ev.start.getTime() <= now && ev.end.getTime() > now) s.add(ev.id);
|
|
219
|
+
}
|
|
220
|
+
return s;
|
|
201
221
|
});
|
|
202
222
|
const nowFracHour = $derived((clock.tick - clock.today) / HOUR_MS);
|
|
223
|
+
/** Y offset of the now-line, or null when outside visibleHours */
|
|
203
224
|
const nowY = $derived.by(() => {
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
if (nowFracHour < startHour || nowFracHour > endHour) return null;
|
|
226
|
+
return (nowFracHour - startHour) * HOUR_H;
|
|
206
227
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
228
|
+
// ── Empty week ─────────────────────────────────────
|
|
229
|
+
const weekIsEmpty = $derived(!events.some((ev) => ev.start.getTime() < weekEndMs && ev.end.getTime() > weekStartMs));
|
|
230
|
+
// ── Auto-scroll (mount + week change) ──────────────
|
|
231
|
+
// Week with today → current time minus ~1h of context; otherwise the
|
|
232
|
+
// visibleHours start or 08:00, whichever is later.
|
|
210
233
|
$effect(() => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
void weekStartMs;
|
|
235
|
+
const el = scrollEl;
|
|
236
|
+
if (!el) return;
|
|
237
|
+
untrack(() => {
|
|
238
|
+
let targetHour;
|
|
239
|
+
if (weekHasToday) {
|
|
240
|
+
const clamped = Math.min(Math.max(nowFracHour, startHour), endHour);
|
|
241
|
+
targetHour = Math.max(startHour, clamped - 1);
|
|
242
|
+
} else {
|
|
243
|
+
targetHour = Math.max(startHour, Math.min(8, endHour - 1));
|
|
244
|
+
}
|
|
245
|
+
el.scrollTop = (targetHour - startHour) * HOUR_H;
|
|
246
|
+
});
|
|
224
247
|
});
|
|
248
|
+
// ── Pointer coalescing ─────────────────────────────
|
|
249
|
+
// Pointer events fire far faster than the screen repaints (120–1000 Hz on
|
|
250
|
+
// modern mice). Handling each one wrote drag state → Svelte wrote inline
|
|
251
|
+
// styles → the next handler's getBoundingClientRect() forced a synchronous
|
|
252
|
+
// layout: classic read-after-write thrash, and the reason a drag felt heavy
|
|
253
|
+
// on a busy grid. Running at most one handler per frame, off the newest
|
|
254
|
+
// event, with the grid rect measured once inside that frame, removes both.
|
|
225
255
|
let rafId = 0;
|
|
226
256
|
let rafRun = null;
|
|
227
257
|
let rectCache = null;
|
|
228
258
|
function perFrame(handler) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
return (e) => {
|
|
260
|
+
rafRun = () => {
|
|
261
|
+
rectCache = colsEl?.getBoundingClientRect() ?? null;
|
|
262
|
+
try {
|
|
263
|
+
handler(e);
|
|
264
|
+
} finally {
|
|
265
|
+
rectCache = null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
if (rafId) return;
|
|
269
|
+
rafId = requestAnimationFrame(flushFrame);
|
|
270
|
+
};
|
|
241
271
|
}
|
|
272
|
+
/** Run a pending move now — pointerup must not drop the last frame. */
|
|
242
273
|
function flushFrame() {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
274
|
+
if (rafId) {
|
|
275
|
+
cancelAnimationFrame(rafId);
|
|
276
|
+
rafId = 0;
|
|
277
|
+
}
|
|
278
|
+
const run = rafRun;
|
|
279
|
+
rafRun = null;
|
|
280
|
+
run?.();
|
|
250
281
|
}
|
|
251
282
|
function cancelFrame() {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
283
|
+
if (rafId) {
|
|
284
|
+
cancelAnimationFrame(rafId);
|
|
285
|
+
rafId = 0;
|
|
286
|
+
}
|
|
287
|
+
rafRun = null;
|
|
257
288
|
}
|
|
289
|
+
// ── Geometry helpers ───────────────────────────────
|
|
258
290
|
function colsRect() {
|
|
259
|
-
|
|
291
|
+
return rectCache ?? colsEl.getBoundingClientRect();
|
|
260
292
|
}
|
|
293
|
+
/** Pointer X → index into dayCols (clamped) */
|
|
261
294
|
function pointerDayIndex(clientX) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
295
|
+
const r = colsRect();
|
|
296
|
+
const n = dayCols.length;
|
|
297
|
+
if (n === 0) return 0;
|
|
298
|
+
const w = r.width / n;
|
|
299
|
+
return Math.max(0, Math.min(n - 1, Math.floor((clientX - r.left) / w)));
|
|
267
300
|
}
|
|
301
|
+
/** Pointer Y → fractional hour (unclamped) */
|
|
268
302
|
function pointerHour(clientY) {
|
|
269
|
-
|
|
303
|
+
return startHour + (clientY - colsRect().top) / HOUR_H;
|
|
270
304
|
}
|
|
305
|
+
/** Pointer → epoch ms (day from X, time from Y, clamped into the band) */
|
|
271
306
|
function pointerTimeMs(clientX, clientY) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
307
|
+
const dayMs = dayCols[pointerDayIndex(clientX)]?.ms ?? weekStartMs;
|
|
308
|
+
const hour = Math.min(Math.max(pointerHour(clientY), startHour), endHour);
|
|
309
|
+
return dayMs + hour * HOUR_MS;
|
|
275
310
|
}
|
|
311
|
+
/** Clamp a timestamp into the visible band of a specific day */
|
|
276
312
|
function clampToDayBand(ms, dayMs) {
|
|
277
|
-
|
|
313
|
+
return Math.max(dayMs + startHour * HOUR_MS, Math.min(dayMs + endHour * HOUR_MS, ms));
|
|
278
314
|
}
|
|
315
|
+
// ── Blocked slot helpers ───────────────────────────
|
|
279
316
|
function isBlockedAt(dayMs, hour) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
317
|
+
if (!blockedSlots?.length) return false;
|
|
318
|
+
const jsDay = new Date(dayMs).getDay();
|
|
319
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
320
|
+
return blockedSlots.some((slot) => {
|
|
321
|
+
if (slot.day && slot.day !== isoDay) return false;
|
|
322
|
+
return hour >= slot.start && hour < slot.end;
|
|
323
|
+
});
|
|
287
324
|
}
|
|
288
325
|
function blockedRangeLabel(dayMs, slotStart, slotEnd) {
|
|
289
|
-
|
|
326
|
+
return `${fmtTime(new Date(dayMs + slotStart * HOUR_MS), locale)} – ${fmtTime(new Date(dayMs + slotEnd * HOUR_MS), locale)}`;
|
|
290
327
|
}
|
|
328
|
+
// ── Status label (aria) ────────────────────────────
|
|
291
329
|
function statusText(ev) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
330
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
331
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
332
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
333
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
334
|
+
return "";
|
|
297
335
|
}
|
|
298
336
|
function ghostForDay(dayMs) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
if (!drag?.active || !drag.payload) return null;
|
|
338
|
+
const mode = drag.mode;
|
|
339
|
+
if (mode !== "move" && mode !== "create") return null;
|
|
340
|
+
const s = drag.payload.start.getTime();
|
|
341
|
+
const e = drag.payload.end.getTime();
|
|
342
|
+
const bandS = dayMs + startHour * HOUR_MS;
|
|
343
|
+
const bandE = dayMs + endHour * HOUR_MS;
|
|
344
|
+
const cs = Math.max(s, bandS);
|
|
345
|
+
const ce = Math.min(e, bandE);
|
|
346
|
+
if (ce <= cs) return null;
|
|
347
|
+
return {
|
|
348
|
+
top: ((cs - dayMs) / HOUR_MS - startHour) * HOUR_H,
|
|
349
|
+
height: Math.max(12, (ce - cs) / HOUR_MS * HOUR_H),
|
|
350
|
+
start: drag.payload.start,
|
|
351
|
+
end: drag.payload.end,
|
|
352
|
+
create: mode === "create",
|
|
353
|
+
// Only the segment containing the start shows the readout
|
|
354
|
+
showTime: cs === Math.max(s, dayMs)
|
|
355
|
+
};
|
|
318
356
|
}
|
|
357
|
+
// ── Drag-to-create (mouse: sweep; touch: long-press) ──
|
|
358
|
+
// Mouse movement below CREATE_THRESHOLD stays a plain click. On touch a
|
|
359
|
+
// vertical drag must remain a scroll, so create only arms after a ~350ms
|
|
360
|
+
// still hold (moving > 8px first cancels it).
|
|
319
361
|
const CREATE_THRESHOLD = 4;
|
|
320
362
|
const LONG_PRESS_MS = 350;
|
|
321
363
|
const LONG_PRESS_TOLERANCE = 8;
|
|
@@ -326,116 +368,122 @@ let crAnchorMs = 0;
|
|
|
326
368
|
let crDayMs = 0;
|
|
327
369
|
let crStarted = false;
|
|
328
370
|
let longPressTimer = null;
|
|
371
|
+
// touch-action can't change mid-gesture, so once a touch drag-create/resize
|
|
372
|
+
// is live we block native scrolling with a non-passive touchmove listener.
|
|
329
373
|
function blockTouchScroll(e) {
|
|
330
|
-
|
|
374
|
+
e.preventDefault();
|
|
331
375
|
}
|
|
332
376
|
function addTouchScrollBlock() {
|
|
333
|
-
|
|
377
|
+
window.addEventListener("touchmove", blockTouchScroll, { passive: false });
|
|
334
378
|
}
|
|
335
379
|
function removeTouchScrollBlock() {
|
|
336
|
-
|
|
380
|
+
window.removeEventListener("touchmove", blockTouchScroll);
|
|
337
381
|
}
|
|
338
382
|
function clearLongPress() {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
383
|
+
if (longPressTimer !== null) {
|
|
384
|
+
clearTimeout(longPressTimer);
|
|
385
|
+
longPressTimer = null;
|
|
386
|
+
}
|
|
343
387
|
}
|
|
344
388
|
function startColsCreate() {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
389
|
+
if (!drag) return;
|
|
390
|
+
crStarted = true;
|
|
391
|
+
crAnchorMs = clampToDayBand(Math.floor(crAnchorMs / SNAP_MS) * SNAP_MS, crDayMs);
|
|
392
|
+
drag.beginCreate(new Date(crAnchorMs), new Date(crAnchorMs + SNAP_MS));
|
|
393
|
+
addTouchScrollBlock();
|
|
350
394
|
}
|
|
351
395
|
function onColsPointerDown(e) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
window.addEventListener("pointerup", onColsCreateUp, { once: true });
|
|
373
|
-
window.addEventListener("pointercancel", onColsCreateCancel, { once: true });
|
|
396
|
+
if (e.button !== 0 || !drag || !oneventcreate || readOnly) return;
|
|
397
|
+
if (e.target.closest(".tw-ev, .tw-ghost")) return;
|
|
398
|
+
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
399
|
+
if (!day || day.isDisabled) return;
|
|
400
|
+
// Never arm a create on a blocked region (Calendar validates on commit too)
|
|
401
|
+
if (isBlockedAt(day.ms, Math.min(Math.max(pointerHour(e.clientY), startHour), endHour))) return;
|
|
402
|
+
crStartX = e.clientX;
|
|
403
|
+
crStartY = e.clientY;
|
|
404
|
+
crDayMs = day.ms;
|
|
405
|
+
crAnchorMs = clampToDayBand(day.ms + Math.max(pointerHour(e.clientY), startHour) * HOUR_MS, day.ms);
|
|
406
|
+
crStarted = false;
|
|
407
|
+
if (e.pointerType === "touch") {
|
|
408
|
+
longPressTimer = setTimeout(() => {
|
|
409
|
+
longPressTimer = null;
|
|
410
|
+
startColsCreate();
|
|
411
|
+
}, LONG_PRESS_MS);
|
|
412
|
+
}
|
|
413
|
+
window.addEventListener("pointermove", onColsCreateMove);
|
|
414
|
+
window.addEventListener("pointerup", onColsCreateUp, { once: true });
|
|
415
|
+
window.addEventListener("pointercancel", onColsCreateCancel, { once: true });
|
|
374
416
|
}
|
|
375
417
|
const onColsCreateMove = perFrame((e) => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
new Date(Math.max(crAnchorMs + SNAP_MS, snapped))
|
|
392
|
-
);
|
|
418
|
+
if (!drag) return;
|
|
419
|
+
if (!crStarted) {
|
|
420
|
+
if (longPressTimer !== null) {
|
|
421
|
+
// Finger moved before the hold completed → it's a scroll
|
|
422
|
+
const moved = Math.hypot(e.clientX - crStartX, e.clientY - crStartY);
|
|
423
|
+
if (moved > LONG_PRESS_TOLERANCE) cleanupColsCreate();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (e.pointerType === "touch") return;
|
|
427
|
+
if (Math.abs(e.clientY - crStartY) < CREATE_THRESHOLD) return;
|
|
428
|
+
startColsCreate();
|
|
429
|
+
}
|
|
430
|
+
const raw = crDayMs + pointerHour(e.clientY) * HOUR_MS;
|
|
431
|
+
const snapped = clampToDayBand(Math.round(raw / SNAP_MS) * SNAP_MS, crDayMs);
|
|
432
|
+
drag.updatePointer(new Date(Math.min(crAnchorMs, snapped)), new Date(Math.max(crAnchorMs + SNAP_MS, snapped)));
|
|
393
433
|
});
|
|
394
434
|
function cleanupColsCreate() {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
435
|
+
cancelFrame();
|
|
436
|
+
clearLongPress();
|
|
437
|
+
removeTouchScrollBlock();
|
|
438
|
+
window.removeEventListener("pointermove", onColsCreateMove);
|
|
439
|
+
window.removeEventListener("pointerup", onColsCreateUp);
|
|
440
|
+
window.removeEventListener("pointercancel", onColsCreateCancel);
|
|
441
|
+
crStarted = false;
|
|
402
442
|
}
|
|
403
443
|
function onColsCreateUp() {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
444
|
+
// Commit the newest pointer position, not the last one that made it to a
|
|
445
|
+
// frame. Only once the gesture is live — flushing a pending frame before
|
|
446
|
+
// that could start (and instantly commit) a drag the user meant as a click.
|
|
447
|
+
if (crStarted) flushFrame();
|
|
448
|
+
if (drag && crStarted) {
|
|
449
|
+
// The click event (if any) fires synchronously after pointerup
|
|
450
|
+
suppressColsClick = true;
|
|
451
|
+
commitDragCtx?.();
|
|
452
|
+
setTimeout(() => {
|
|
453
|
+
suppressColsClick = false;
|
|
454
|
+
}, 0);
|
|
455
|
+
}
|
|
456
|
+
cleanupColsCreate();
|
|
413
457
|
}
|
|
414
458
|
function onColsCreateCancel() {
|
|
415
|
-
|
|
416
|
-
|
|
459
|
+
if (drag && crStarted) drag.cancel();
|
|
460
|
+
cleanupColsCreate();
|
|
417
461
|
}
|
|
418
462
|
function onColsContextMenu(e) {
|
|
419
|
-
|
|
463
|
+
// Long-press on touch fires contextmenu on some platforms — keep it
|
|
464
|
+
// from interrupting an armed or pending drag-create.
|
|
465
|
+
if (crStarted || longPressTimer !== null) e.preventDefault();
|
|
420
466
|
}
|
|
467
|
+
// ── Click-to-create (plain click → default slot) ───
|
|
421
468
|
function handleColsClick(e) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
469
|
+
if (suppressColsClick) {
|
|
470
|
+
suppressColsClick = false;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!oneventcreate || readOnly) return;
|
|
474
|
+
if (e.target.closest(".tw-ev, .tw-ghost")) return;
|
|
475
|
+
const day = dayCols[pointerDayIndex(e.clientX)];
|
|
476
|
+
if (!day || day.isDisabled) return;
|
|
477
|
+
const hour = Math.min(Math.max(pointerHour(e.clientY), startHour), endHour);
|
|
478
|
+
if (isBlockedAt(day.ms, hour)) return;
|
|
479
|
+
const startMs = clampToDayBand(Math.floor((day.ms + hour * HOUR_MS) / SNAP_MS) * SNAP_MS, day.ms);
|
|
480
|
+
const durMin = minDuration ?? 60;
|
|
481
|
+
oneventcreate({
|
|
482
|
+
start: new Date(startMs),
|
|
483
|
+
end: new Date(startMs + durMin * 6e4)
|
|
484
|
+
});
|
|
438
485
|
}
|
|
486
|
+
// ── Drag-to-move (vertical = time, horizontal = day) ──
|
|
439
487
|
const DRAG_THRESHOLD = 5;
|
|
440
488
|
let evDragStartX = 0;
|
|
441
489
|
let evDragStartY = 0;
|
|
@@ -443,134 +491,145 @@ let evGrabOffsetMs = 0;
|
|
|
443
491
|
let evDragStarted = false;
|
|
444
492
|
let evDragMovable = false;
|
|
445
493
|
let evDragEvent = null;
|
|
494
|
+
// The block's rect when the pointer went down — a click hands it to the
|
|
495
|
+
// host as the anchor for whatever it opens.
|
|
496
|
+
let evAnchor;
|
|
446
497
|
function onEventPointerDown(e, ev) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
498
|
+
if (e.button !== 0) return;
|
|
499
|
+
e.stopPropagation();
|
|
500
|
+
evAnchor = e.currentTarget.getBoundingClientRect();
|
|
501
|
+
// Even non-movable events (readOnly view, per-event readOnly) go through
|
|
502
|
+
// the pointerup path so a plain click still opens them.
|
|
503
|
+
evDragMovable = !!drag && !readOnly && !ev.data?.readOnly;
|
|
504
|
+
evDragStartX = e.clientX;
|
|
505
|
+
evDragStartY = e.clientY;
|
|
506
|
+
evGrabOffsetMs = pointerTimeMs(e.clientX, e.clientY) - ev.start.getTime();
|
|
507
|
+
evDragStarted = false;
|
|
508
|
+
evDragEvent = ev;
|
|
509
|
+
window.addEventListener("pointermove", onEvMove);
|
|
510
|
+
window.addEventListener("pointerup", onEvUp, { once: true });
|
|
511
|
+
window.addEventListener("pointercancel", onEvCancel, { once: true });
|
|
458
512
|
}
|
|
459
513
|
const onEvMove = perFrame((e) => {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
514
|
+
const ev = evDragEvent;
|
|
515
|
+
if (!drag || !ev || !evDragMovable) return;
|
|
516
|
+
if (!evDragStarted) {
|
|
517
|
+
const moved = Math.abs(e.clientX - evDragStartX) + Math.abs(e.clientY - evDragStartY);
|
|
518
|
+
if (moved < DRAG_THRESHOLD) return;
|
|
519
|
+
evDragStarted = true;
|
|
520
|
+
drag.beginMove(ev.id, ev.start, ev.end);
|
|
521
|
+
}
|
|
522
|
+
const duration = ev.end.getTime() - ev.start.getTime();
|
|
523
|
+
const raw = pointerTimeMs(e.clientX, e.clientY) - evGrabOffsetMs;
|
|
524
|
+
const snapped = Math.round(raw / SNAP_MS) * SNAP_MS;
|
|
525
|
+
drag.updatePointer(new Date(snapped), new Date(snapped + duration));
|
|
472
526
|
});
|
|
473
527
|
function cleanupEvDrag() {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
528
|
+
cancelFrame();
|
|
529
|
+
window.removeEventListener("pointermove", onEvMove);
|
|
530
|
+
window.removeEventListener("pointerup", onEvUp);
|
|
531
|
+
window.removeEventListener("pointercancel", onEvCancel);
|
|
532
|
+
evDragStarted = false;
|
|
533
|
+
evDragMovable = false;
|
|
534
|
+
evDragEvent = null;
|
|
481
535
|
}
|
|
482
536
|
function onEvUp() {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
537
|
+
if (evDragStarted) flushFrame();
|
|
538
|
+
if (!evDragStarted && evDragEvent) {
|
|
539
|
+
oneventclick?.(evDragEvent, evAnchor);
|
|
540
|
+
} else if (evDragStarted && drag) {
|
|
541
|
+
// If the pointer ends off the block, the synthesized click lands on
|
|
542
|
+
// the grid — suppress the click-to-create it would trigger.
|
|
543
|
+
suppressColsClick = true;
|
|
544
|
+
commitDragCtx?.();
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
suppressColsClick = false;
|
|
547
|
+
}, 0);
|
|
548
|
+
}
|
|
549
|
+
cleanupEvDrag();
|
|
494
550
|
}
|
|
495
551
|
function onEvCancel() {
|
|
496
|
-
|
|
497
|
-
|
|
552
|
+
if (drag && evDragStarted) drag.cancel();
|
|
553
|
+
cleanupEvDrag();
|
|
498
554
|
}
|
|
555
|
+
// ── Resize (top/bottom edge handles) ───────────────
|
|
499
556
|
let rsStartY = 0;
|
|
500
557
|
let rsStarted = false;
|
|
501
558
|
let rsEdge = "end";
|
|
502
559
|
let rsEvent = null;
|
|
503
560
|
function onResizePointerDown(e, ev, edge) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
561
|
+
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
562
|
+
e.stopPropagation();
|
|
563
|
+
evAnchor = e.currentTarget.parentElement?.getBoundingClientRect();
|
|
564
|
+
rsStartY = e.clientY;
|
|
565
|
+
rsStarted = false;
|
|
566
|
+
rsEdge = edge;
|
|
567
|
+
rsEvent = ev;
|
|
568
|
+
window.addEventListener("pointermove", onResizeMove);
|
|
569
|
+
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
570
|
+
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
513
571
|
}
|
|
514
572
|
const onResizeMove = perFrame((e) => {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
573
|
+
const ev = rsEvent;
|
|
574
|
+
if (!drag || !ev) return;
|
|
575
|
+
if (!rsStarted) {
|
|
576
|
+
if (Math.abs(e.clientY - rsStartY) < CREATE_THRESHOLD) return;
|
|
577
|
+
rsStarted = true;
|
|
578
|
+
drag.beginResize(ev.id, rsEdge, ev.start, ev.end);
|
|
579
|
+
addTouchScrollBlock();
|
|
580
|
+
}
|
|
581
|
+
const evDayMs = sod(ev.start.getTime());
|
|
582
|
+
const raw = evDayMs + pointerHour(e.clientY) * HOUR_MS;
|
|
583
|
+
const snapped = clampToDayBand(Math.round(raw / SNAP_MS) * SNAP_MS, evDayMs);
|
|
584
|
+
if (rsEdge === "end") {
|
|
585
|
+
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
586
|
+
drag.updatePointer(ev.start, new Date(end));
|
|
587
|
+
} else {
|
|
588
|
+
const start = Math.min(snapped, ev.end.getTime() - SNAP_MS);
|
|
589
|
+
drag.updatePointer(new Date(start), ev.end);
|
|
590
|
+
}
|
|
533
591
|
});
|
|
534
592
|
function cleanupResize() {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
593
|
+
cancelFrame();
|
|
594
|
+
removeTouchScrollBlock();
|
|
595
|
+
window.removeEventListener("pointermove", onResizeMove);
|
|
596
|
+
window.removeEventListener("pointerup", onResizeUp);
|
|
597
|
+
window.removeEventListener("pointercancel", onResizeCancel);
|
|
598
|
+
rsStarted = false;
|
|
599
|
+
rsEvent = null;
|
|
542
600
|
}
|
|
543
601
|
function onResizeUp() {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
602
|
+
if (rsStarted) flushFrame();
|
|
603
|
+
if (drag && rsStarted) {
|
|
604
|
+
// If the pointer ends off the block, the synthesized click lands on
|
|
605
|
+
// the grid — suppress the click-to-create it would trigger.
|
|
606
|
+
suppressColsClick = true;
|
|
607
|
+
commitDragCtx?.();
|
|
608
|
+
setTimeout(() => {
|
|
609
|
+
suppressColsClick = false;
|
|
610
|
+
}, 0);
|
|
611
|
+
} else if (rsEvent && !rsStarted) {
|
|
612
|
+
oneventclick?.(rsEvent, evAnchor);
|
|
613
|
+
}
|
|
614
|
+
cleanupResize();
|
|
555
615
|
}
|
|
556
616
|
function onResizeCancel() {
|
|
557
|
-
|
|
558
|
-
|
|
617
|
+
if (drag && rsStarted) drag.cancel();
|
|
618
|
+
cleanupResize();
|
|
559
619
|
}
|
|
620
|
+
// ── Escape cancels any in-flight drag ──────────────
|
|
560
621
|
function onWindowKeydown(e) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
{ once: true }
|
|
573
|
-
);
|
|
622
|
+
if (e.key !== "Escape" || !drag?.active) return;
|
|
623
|
+
drag.cancel();
|
|
624
|
+
cleanupColsCreate();
|
|
625
|
+
cleanupEvDrag();
|
|
626
|
+
cleanupResize();
|
|
627
|
+
// Suppress the click the trailing pointerup would synthesize on the
|
|
628
|
+
// grid (it would otherwise create an event).
|
|
629
|
+
suppressColsClick = true;
|
|
630
|
+
window.addEventListener("pointerup", () => setTimeout(() => {
|
|
631
|
+
suppressColsClick = false;
|
|
632
|
+
}, 0), { once: true });
|
|
574
633
|
}
|
|
575
634
|
</script>
|
|
576
635
|
|
|
@@ -762,7 +821,8 @@ function onWindowKeydown(e) {
|
|
|
762
821
|
aria-label="{p.ev.title}, {fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}, {fmtDuration(p.ev.start, p.ev.end)}{statusText(p.ev)}{isCurrent ? ` (${L.inProgress})` : ''}"
|
|
763
822
|
onpointerdown={(e) => onEventPointerDown(e, p.ev)}
|
|
764
823
|
onpointerenter={() => oneventhover?.(p.ev)}
|
|
765
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(p.ev); } }}
|
|
824
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(p.ev, e.currentTarget.getBoundingClientRect()); } }}
|
|
825
|
+
|
|
766
826
|
>
|
|
767
827
|
<div class="tw-ev-stripe" aria-hidden="true"></div>
|
|
768
828
|
<div class="tw-ev-body">
|
|
@@ -1334,10 +1394,16 @@ function onWindowKeydown(e) {
|
|
|
1334
1394
|
}
|
|
1335
1395
|
|
|
1336
1396
|
/* ─── Resize handles ─────────────────────────────── */
|
|
1397
|
+
/* Resizing lives only on the centered grip column. The old full-width
|
|
1398
|
+
edge bands (12–20px of inward slop each) covered short events
|
|
1399
|
+
entirely — min block height is 24px, so every grab meant to move
|
|
1400
|
+
started a resize instead. The grip is the visible affordance;
|
|
1401
|
+
everything else on the block drags to move. */
|
|
1337
1402
|
.tw-ev-handle {
|
|
1338
1403
|
position: absolute;
|
|
1339
|
-
left:
|
|
1340
|
-
|
|
1404
|
+
left: 50%;
|
|
1405
|
+
transform: translateX(-50%);
|
|
1406
|
+
width: 44px;
|
|
1341
1407
|
height: 8px;
|
|
1342
1408
|
z-index: 2;
|
|
1343
1409
|
cursor: ns-resize;
|
|
@@ -1377,6 +1443,8 @@ function onWindowKeydown(e) {
|
|
|
1377
1443
|
.tw-ev:focus-visible .tw-ev-handle::after,
|
|
1378
1444
|
.tw-ev--resizing .tw-ev-handle::after,
|
|
1379
1445
|
.tw-ev--selected .tw-ev-handle::after { opacity: 0.55; }
|
|
1446
|
+
/* Pointer on the grip column itself: brighten so the hit zone reads */
|
|
1447
|
+
.tw-ev-handle:hover::after { opacity: 0.9; }
|
|
1380
1448
|
/* Coarse pointers can't hover — show the grips persistently */
|
|
1381
1449
|
@media (hover: none) {
|
|
1382
1450
|
.tw-ev-handle::after { opacity: 0.55; }
|