@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
|
@@ -12,18 +12,8 @@ import { createClock } from "../../core/clock.svelte.js";
|
|
|
12
12
|
import { DAY_MS, HOUR_MS, sod, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
|
|
13
13
|
import { fmtH, fmtTime } from "../../core/locale.js";
|
|
14
14
|
import { createSwipe } from "./swipe.js";
|
|
15
|
-
let {
|
|
16
|
-
|
|
17
|
-
events = [],
|
|
18
|
-
style = "",
|
|
19
|
-
locale,
|
|
20
|
-
focusDate,
|
|
21
|
-
oneventclick,
|
|
22
|
-
oneventcreate,
|
|
23
|
-
selectedEventId = null,
|
|
24
|
-
readOnly = false,
|
|
25
|
-
visibleHours
|
|
26
|
-
} = $props();
|
|
15
|
+
let { height = null, events = [], style = "", locale, focusDate, oneventclick, oneventcreate, selectedEventId = null, readOnly = false, visibleHours } = $props();
|
|
16
|
+
// ── Context ────────────────────────────────────────
|
|
27
17
|
const ctx = useCalendarContext();
|
|
28
18
|
const L = $derived(ctx.labels);
|
|
29
19
|
const viewState = $derived(ctx.viewState);
|
|
@@ -37,189 +27,218 @@ const drag = $derived(ctx.drag);
|
|
|
37
27
|
const commitDragCtx = $derived(ctx.commitDrag);
|
|
38
28
|
const SNAP_MS = $derived(ctx.snapInterval * 6e4);
|
|
39
29
|
const clock = createClock(ctx.timezone);
|
|
30
|
+
// ── Config ─────────────────────────────────────────
|
|
40
31
|
const HOUR_HEIGHT = 64;
|
|
41
32
|
const GUTTER_W = 40;
|
|
42
33
|
const startHour = $derived(visibleHours?.[0] ?? 0);
|
|
43
34
|
const endHour = $derived(visibleHours?.[1] ?? 24);
|
|
44
35
|
const hourCount = $derived(Math.max(1, endHour - startHour));
|
|
45
36
|
const gridHeight = $derived(hourCount * HOUR_HEIGHT);
|
|
37
|
+
// ── Day state ──────────────────────────────────────
|
|
46
38
|
const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
|
|
47
39
|
const dayEnd = $derived(dayMs + DAY_MS);
|
|
48
40
|
const isToday = $derived(dayMs === clock.today);
|
|
49
41
|
const isDisabled = $derived(disabledSet.has(dayMs));
|
|
42
|
+
// ── Load range ─────────────────────────────────────
|
|
50
43
|
$effect(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
if (!loadRangeCtx) return;
|
|
45
|
+
const rangeStart = new Date(dayMs - 2 * DAY_MS);
|
|
46
|
+
const rangeEnd = new Date(dayMs + 3 * DAY_MS);
|
|
47
|
+
loadRangeCtx.set({
|
|
48
|
+
start: rangeStart,
|
|
49
|
+
end: rangeEnd
|
|
50
|
+
});
|
|
51
|
+
return () => loadRangeCtx.set(null);
|
|
56
52
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
53
|
+
// ── Events partition ───────────────────────────────
|
|
54
|
+
const timedEvents = $derived(events.filter((ev) => !isAllDay(ev) && !isMultiDay(ev) && ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime()));
|
|
60
55
|
const allDayEvents = $derived.by(() => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
const segs = [];
|
|
57
|
+
for (const ev of events) {
|
|
58
|
+
if (!isAllDay(ev) && !isMultiDay(ev)) continue;
|
|
59
|
+
const seg = segmentForDay(ev, dayMs);
|
|
60
|
+
if (seg) segs.push(seg);
|
|
61
|
+
}
|
|
62
|
+
return segs;
|
|
68
63
|
});
|
|
69
64
|
const isEmpty = $derived(timedEvents.length === 0 && allDayEvents.length === 0);
|
|
65
|
+
// ── All-day strip expansion ────────────────────────
|
|
70
66
|
const ALLDAY_MAX = 3;
|
|
71
67
|
let allDayExpanded = $state(false);
|
|
72
68
|
const visibleAllDay = $derived(allDayExpanded ? allDayEvents : allDayEvents.slice(0, ALLDAY_MAX));
|
|
73
69
|
const positionedEvents = $derived.by(() => {
|
|
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
|
-
|
|
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
|
+
// Find the next upcoming event today
|
|
74
|
+
let nextEventId = null;
|
|
75
|
+
if (isToday) {
|
|
76
|
+
for (const ev of [...sorted].sort((a, b) => a.start.getTime() - b.start.getTime())) {
|
|
77
|
+
const s = ev.start.getTime();
|
|
78
|
+
if (s > now) {
|
|
79
|
+
nextEventId = ev.id;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Overlap grouping
|
|
85
|
+
const infos = sorted.map((ev) => {
|
|
86
|
+
// While resizing, this event tracks the tentative drag payload
|
|
87
|
+
const resizing = rsP?.eventId === ev.id;
|
|
88
|
+
const evStart = resizing ? rsP.start : ev.start;
|
|
89
|
+
const evEnd = resizing ? rsP.end : ev.end;
|
|
90
|
+
const sMs = Math.max(evStart.getTime(), dayMs + startHour * HOUR_MS);
|
|
91
|
+
const eMs = Math.min(evEnd.getTime(), dayMs + endHour * HOUR_MS);
|
|
92
|
+
const topH = (sMs - dayMs) / HOUR_MS - startHour;
|
|
93
|
+
const botH = (eMs - dayMs) / HOUR_MS - startHour;
|
|
94
|
+
return {
|
|
95
|
+
ev,
|
|
96
|
+
top: topH * HOUR_HEIGHT,
|
|
97
|
+
height: Math.max(24, (botH - topH) * HOUR_HEIGHT),
|
|
98
|
+
isCurrent: ev.start.getTime() <= now && ev.end.getTime() > now,
|
|
99
|
+
isNext: ev.id === nextEventId,
|
|
100
|
+
isResizing: resizing,
|
|
101
|
+
startMs: sMs,
|
|
102
|
+
endMs: eMs,
|
|
103
|
+
col: 0,
|
|
104
|
+
totalCols: 1
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
// Assign columns for overlapping events
|
|
108
|
+
const par = infos.map((_, i) => i);
|
|
109
|
+
function find(i) {
|
|
110
|
+
while (par[i] !== i) {
|
|
111
|
+
par[i] = par[par[i]];
|
|
112
|
+
i = par[i];
|
|
113
|
+
}
|
|
114
|
+
return i;
|
|
115
|
+
}
|
|
116
|
+
for (let i = 0; i < infos.length; i++) {
|
|
117
|
+
for (let j = i + 1; j < infos.length; j++) {
|
|
118
|
+
if (infos[j].startMs < infos[i].endMs) par[find(i)] = find(j);
|
|
119
|
+
else break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const groups = new Map();
|
|
123
|
+
for (let i = 0; i < infos.length; i++) {
|
|
124
|
+
const root = find(i);
|
|
125
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
126
|
+
groups.get(root).push(i);
|
|
127
|
+
}
|
|
128
|
+
for (const [, indices] of groups) {
|
|
129
|
+
const rows = [];
|
|
130
|
+
for (const idx of indices) {
|
|
131
|
+
let row = 0;
|
|
132
|
+
for (let r = 0; r < rows.length; r++) {
|
|
133
|
+
if (rows[r] <= infos[idx].startMs) {
|
|
134
|
+
row = r;
|
|
135
|
+
rows[r] = infos[idx].endMs;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
row = r + 1;
|
|
139
|
+
}
|
|
140
|
+
if (row >= rows.length) rows.push(infos[idx].endMs);
|
|
141
|
+
infos[idx].col = row;
|
|
142
|
+
}
|
|
143
|
+
for (const idx of indices) infos[idx].totalCols = rows.length;
|
|
144
|
+
}
|
|
145
|
+
return infos.map((info) => ({
|
|
146
|
+
ev: info.ev,
|
|
147
|
+
top: info.top,
|
|
148
|
+
height: info.height,
|
|
149
|
+
left: `calc(${GUTTER_W}px + ${info.col / info.totalCols * 100}% - ${GUTTER_W * info.col / info.totalCols}px)`,
|
|
150
|
+
width: `calc(${100 / info.totalCols}% - ${GUTTER_W / info.totalCols + 2}px)`,
|
|
151
|
+
isCurrent: info.isCurrent,
|
|
152
|
+
isNext: info.isNext,
|
|
153
|
+
isResizing: info.isResizing,
|
|
154
|
+
col: info.col,
|
|
155
|
+
totalCols: info.totalCols
|
|
156
|
+
}));
|
|
157
157
|
});
|
|
158
|
+
// ── Now indicator ──────────────────────────────────
|
|
158
159
|
const nowOffset = $derived.by(() => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
if (!isToday) return -1;
|
|
161
|
+
const h = (clock.tick - dayMs) / HOUR_MS - startHour;
|
|
162
|
+
if (h < 0 || h > hourCount) return -1;
|
|
163
|
+
return h * HOUR_HEIGHT;
|
|
163
164
|
});
|
|
165
|
+
// ── Blocked slot check ─────────────────────────────
|
|
164
166
|
function isBlockedAt(hour) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
if (!blockedSlots?.length) return false;
|
|
168
|
+
const jsDay = new Date(dayMs).getDay();
|
|
169
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
170
|
+
return blockedSlots.some((slot) => {
|
|
171
|
+
if (slot.day && slot.day !== isoDay) return false;
|
|
172
|
+
return hour >= slot.start && hour < slot.end;
|
|
173
|
+
});
|
|
172
174
|
}
|
|
175
|
+
// ── Status label (aria) ────────────────────────────
|
|
173
176
|
function statusText(ev) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
178
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
179
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
180
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
181
|
+
return "";
|
|
179
182
|
}
|
|
183
|
+
// ── Touch swipe navigation ─────────────────────────
|
|
180
184
|
let swipeOffset = $state(0);
|
|
181
185
|
let swipeAnimate = $state(false);
|
|
182
186
|
const swipe = createSwipe({
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
187
|
+
disabled: () => !!drag?.active || mbCreateStarted || mbRsStarted || longPressTimer !== null,
|
|
188
|
+
onmove: (dx) => {
|
|
189
|
+
swipeAnimate = false;
|
|
190
|
+
swipeOffset = dx;
|
|
191
|
+
},
|
|
192
|
+
onend: (dir) => {
|
|
193
|
+
if (dir !== 0) {
|
|
194
|
+
// Committed — the day changes, so jump rather than slide new content.
|
|
195
|
+
swipeAnimate = false;
|
|
196
|
+
swipeOffset = 0;
|
|
197
|
+
if (dir > 0) viewState?.prev();
|
|
198
|
+
else viewState?.next();
|
|
199
|
+
} else {
|
|
200
|
+
// Snap back (CSS drops the transition under prefers-reduced-motion).
|
|
201
|
+
swipeAnimate = true;
|
|
202
|
+
swipeOffset = 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
199
205
|
});
|
|
206
|
+
// ── Tap-to-create ──────────────────────────────────
|
|
200
207
|
function handleGridClick(e) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
if (suppressGridClick) {
|
|
209
|
+
suppressGridClick = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!oneventcreate || readOnly || isDisabled) return;
|
|
213
|
+
if (e.target.closest(".mb-event")) return;
|
|
214
|
+
const tMs = gridTimeMs(e.clientY);
|
|
215
|
+
if (isBlockedAt((tMs - dayMs) / HOUR_MS)) return;
|
|
216
|
+
const startMs = clampToDay(Math.floor(tMs / SNAP_MS) * SNAP_MS);
|
|
217
|
+
const durMin = minDuration ?? 60;
|
|
218
|
+
oneventcreate({
|
|
219
|
+
start: new Date(startMs),
|
|
220
|
+
end: new Date(startMs + durMin * 6e4)
|
|
221
|
+
});
|
|
212
222
|
}
|
|
223
|
+
// Keyboard create-at-default: next snap boundary from now (today),
|
|
224
|
+
// otherwise the first visible hour.
|
|
213
225
|
function onGridKeydown(e) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
227
|
+
if (!oneventcreate || readOnly || isDisabled) return;
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
const raw = isToday ? clock.tick : dayMs + startHour * HOUR_MS;
|
|
230
|
+
const startMs = clampToDay(Math.ceil(raw / SNAP_MS) * SNAP_MS);
|
|
231
|
+
if (isBlockedAt((startMs - dayMs) / HOUR_MS)) return;
|
|
232
|
+
const durMin = minDuration ?? 60;
|
|
233
|
+
oneventcreate({
|
|
234
|
+
start: new Date(startMs),
|
|
235
|
+
end: new Date(startMs + durMin * 6e4)
|
|
236
|
+
});
|
|
222
237
|
}
|
|
238
|
+
// ── Drag-to-create (mouse: sweep; touch: long-press, then sweep) ──
|
|
239
|
+
// Mouse movement below CREATE_THRESHOLD stays a plain tap (handleGridClick).
|
|
240
|
+
// On touch, a vertical drag must remain a scroll, so drag-create only arms
|
|
241
|
+
// after a ~350ms still hold (moving > 8px first cancels it).
|
|
223
242
|
const CREATE_THRESHOLD = 4;
|
|
224
243
|
const LONG_PRESS_MS = 350;
|
|
225
244
|
const LONG_PRESS_TOLERANCE = 8;
|
|
@@ -229,162 +248,172 @@ let mbCreateStartY = 0;
|
|
|
229
248
|
let mbCreateAnchorMs = 0;
|
|
230
249
|
let mbCreateStarted = false;
|
|
231
250
|
let longPressTimer = null;
|
|
251
|
+
// touch-action can't change mid-gesture, so once a touch drag-create/resize
|
|
252
|
+
// is live we block native scrolling with a non-passive touchmove listener.
|
|
232
253
|
function blockTouchScroll(e) {
|
|
233
|
-
|
|
254
|
+
e.preventDefault();
|
|
234
255
|
}
|
|
235
256
|
function addTouchScrollBlock() {
|
|
236
|
-
|
|
257
|
+
window.addEventListener("touchmove", blockTouchScroll, { passive: false });
|
|
237
258
|
}
|
|
238
259
|
function removeTouchScrollBlock() {
|
|
239
|
-
|
|
260
|
+
window.removeEventListener("touchmove", blockTouchScroll);
|
|
240
261
|
}
|
|
241
262
|
function clearLongPress() {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
if (longPressTimer !== null) {
|
|
264
|
+
clearTimeout(longPressTimer);
|
|
265
|
+
longPressTimer = null;
|
|
266
|
+
}
|
|
246
267
|
}
|
|
268
|
+
/** Pointer Y → epoch ms within the day grid (accounts for scroll). */
|
|
247
269
|
function gridTimeMs(clientY) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
270
|
+
const rect = gridEl.getBoundingClientRect();
|
|
271
|
+
const y = clientY - rect.top + gridEl.scrollTop;
|
|
272
|
+
return dayMs + (startHour + y / HOUR_HEIGHT) * HOUR_MS;
|
|
251
273
|
}
|
|
274
|
+
/** Clamp a timestamp into the visible hour range of this day. */
|
|
252
275
|
function clampToDay(ms) {
|
|
253
|
-
|
|
276
|
+
return Math.max(dayMs + startHour * HOUR_MS, Math.min(dayMs + endHour * HOUR_MS, ms));
|
|
254
277
|
}
|
|
255
278
|
function startGridCreate() {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
279
|
+
if (!drag) return;
|
|
280
|
+
mbCreateStarted = true;
|
|
281
|
+
mbCreateAnchorMs = clampToDay(Math.floor(mbCreateAnchorMs / SNAP_MS) * SNAP_MS);
|
|
282
|
+
drag.beginCreate(new Date(mbCreateAnchorMs), new Date(mbCreateAnchorMs + SNAP_MS));
|
|
283
|
+
addTouchScrollBlock();
|
|
261
284
|
}
|
|
262
285
|
function onGridPointerDown(e) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
286
|
+
if (e.button !== 0 || !drag || !oneventcreate || readOnly || isDisabled) return;
|
|
287
|
+
if (e.target.closest(".mb-event")) return;
|
|
288
|
+
mbCreateStartX = e.clientX;
|
|
289
|
+
mbCreateStartY = e.clientY;
|
|
290
|
+
mbCreateAnchorMs = gridTimeMs(e.clientY);
|
|
291
|
+
mbCreateStarted = false;
|
|
292
|
+
if (e.pointerType === "touch") {
|
|
293
|
+
longPressTimer = setTimeout(() => {
|
|
294
|
+
longPressTimer = null;
|
|
295
|
+
startGridCreate();
|
|
296
|
+
}, LONG_PRESS_MS);
|
|
297
|
+
}
|
|
298
|
+
window.addEventListener("pointermove", onGridCreateMove);
|
|
299
|
+
window.addEventListener("pointerup", onGridCreateUp, { once: true });
|
|
300
|
+
window.addEventListener("pointercancel", onGridCreateCancel, { once: true });
|
|
278
301
|
}
|
|
279
302
|
function onGridCreateMove(e) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
new Date(Math.max(mbCreateAnchorMs + SNAP_MS, snapped))
|
|
295
|
-
);
|
|
303
|
+
if (!drag) return;
|
|
304
|
+
if (!mbCreateStarted) {
|
|
305
|
+
if (longPressTimer !== null) {
|
|
306
|
+
// Finger moved before the hold completed → it's a scroll/swipe.
|
|
307
|
+
const moved = Math.hypot(e.clientX - mbCreateStartX, e.clientY - mbCreateStartY);
|
|
308
|
+
if (moved > LONG_PRESS_TOLERANCE) cleanupGridCreate();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (e.pointerType === "touch") return;
|
|
312
|
+
if (Math.abs(e.clientY - mbCreateStartY) < CREATE_THRESHOLD) return;
|
|
313
|
+
startGridCreate();
|
|
314
|
+
}
|
|
315
|
+
const snapped = clampToDay(Math.round(gridTimeMs(e.clientY) / SNAP_MS) * SNAP_MS);
|
|
316
|
+
drag.updatePointer(new Date(Math.min(mbCreateAnchorMs, snapped)), new Date(Math.max(mbCreateAnchorMs + SNAP_MS, snapped)));
|
|
296
317
|
}
|
|
297
318
|
function cleanupGridCreate() {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
319
|
+
clearLongPress();
|
|
320
|
+
removeTouchScrollBlock();
|
|
321
|
+
window.removeEventListener("pointermove", onGridCreateMove);
|
|
322
|
+
window.removeEventListener("pointerup", onGridCreateUp);
|
|
323
|
+
window.removeEventListener("pointercancel", onGridCreateCancel);
|
|
324
|
+
mbCreateStarted = false;
|
|
304
325
|
}
|
|
305
326
|
function onGridCreateUp() {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
327
|
+
if (drag && mbCreateStarted) {
|
|
328
|
+
// The click event (if any) fires synchronously after pointerup
|
|
329
|
+
suppressGridClick = true;
|
|
330
|
+
commitDragCtx?.();
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
suppressGridClick = false;
|
|
333
|
+
}, 0);
|
|
334
|
+
}
|
|
335
|
+
cleanupGridCreate();
|
|
314
336
|
}
|
|
315
337
|
function onGridCreateCancel() {
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
// Gesture was taken over by the browser — abandon the tentative create
|
|
339
|
+
if (drag && mbCreateStarted) drag.cancel();
|
|
340
|
+
cleanupGridCreate();
|
|
318
341
|
}
|
|
319
342
|
function onGridContextMenu(e) {
|
|
320
|
-
|
|
343
|
+
// Long-press on touch fires contextmenu on some platforms — keep it
|
|
344
|
+
// from interrupting an armed or pending drag-create.
|
|
345
|
+
if (mbCreateStarted || longPressTimer !== null) e.preventDefault();
|
|
321
346
|
}
|
|
347
|
+
// ── Event resize (top/bottom edge handles) ─────────
|
|
322
348
|
let suppressEventClick = false;
|
|
323
349
|
let mbRsStartY = 0;
|
|
324
350
|
let mbRsStarted = false;
|
|
325
351
|
let mbRsEdge = "end";
|
|
326
352
|
let mbRsEvent = null;
|
|
327
353
|
function onResizePointerDown(e, ev, edge) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
354
|
+
if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
|
|
355
|
+
e.stopPropagation();
|
|
356
|
+
mbRsStartY = e.clientY;
|
|
357
|
+
mbRsStarted = false;
|
|
358
|
+
mbRsEdge = edge;
|
|
359
|
+
mbRsEvent = ev;
|
|
360
|
+
window.addEventListener("pointermove", onResizeMove);
|
|
361
|
+
window.addEventListener("pointerup", onResizeUp, { once: true });
|
|
362
|
+
window.addEventListener("pointercancel", onResizeCancel, { once: true });
|
|
337
363
|
}
|
|
338
364
|
function onResizeMove(e) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
365
|
+
const ev = mbRsEvent;
|
|
366
|
+
if (!drag || !ev) return;
|
|
367
|
+
if (!mbRsStarted) {
|
|
368
|
+
if (Math.abs(e.clientY - mbRsStartY) < CREATE_THRESHOLD) return;
|
|
369
|
+
mbRsStarted = true;
|
|
370
|
+
drag.beginResize(ev.id, mbRsEdge, ev.start, ev.end);
|
|
371
|
+
addTouchScrollBlock();
|
|
372
|
+
}
|
|
373
|
+
const snapped = clampToDay(Math.round(gridTimeMs(e.clientY) / SNAP_MS) * SNAP_MS);
|
|
374
|
+
if (mbRsEdge === "end") {
|
|
375
|
+
const end = Math.max(snapped, ev.start.getTime() + SNAP_MS);
|
|
376
|
+
drag.updatePointer(ev.start, new Date(end));
|
|
377
|
+
} else {
|
|
378
|
+
const start = Math.min(snapped, ev.end.getTime() - SNAP_MS);
|
|
379
|
+
drag.updatePointer(new Date(start), ev.end);
|
|
380
|
+
}
|
|
355
381
|
}
|
|
356
382
|
function cleanupResize() {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
383
|
+
removeTouchScrollBlock();
|
|
384
|
+
window.removeEventListener("pointermove", onResizeMove);
|
|
385
|
+
window.removeEventListener("pointerup", onResizeUp);
|
|
386
|
+
window.removeEventListener("pointercancel", onResizeCancel);
|
|
387
|
+
mbRsStarted = false;
|
|
388
|
+
mbRsEvent = null;
|
|
363
389
|
}
|
|
364
390
|
function onResizeUp() {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
391
|
+
if (drag && mbRsStarted) {
|
|
392
|
+
// The synthesized click can land on the button OR (if the pointer
|
|
393
|
+
// ended off the block) on the grid — suppress both paths.
|
|
394
|
+
suppressEventClick = true;
|
|
395
|
+
suppressGridClick = true;
|
|
396
|
+
commitDragCtx?.();
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
suppressEventClick = false;
|
|
399
|
+
suppressGridClick = false;
|
|
400
|
+
}, 0);
|
|
401
|
+
}
|
|
402
|
+
cleanupResize();
|
|
375
403
|
}
|
|
376
404
|
function onResizeCancel() {
|
|
377
|
-
|
|
378
|
-
|
|
405
|
+
if (drag && mbRsStarted) drag.cancel();
|
|
406
|
+
cleanupResize();
|
|
379
407
|
}
|
|
408
|
+
// ── Auto-scroll to now ─────────────────────────────
|
|
380
409
|
let gridEl;
|
|
381
410
|
$effect(() => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
411
|
+
void dayMs;
|
|
412
|
+
const el = gridEl;
|
|
413
|
+
if (!el) return;
|
|
414
|
+
untrack(() => {
|
|
415
|
+
if (nowOffset >= 0) el.scrollTop = Math.max(0, nowOffset - 120);
|
|
416
|
+
});
|
|
388
417
|
});
|
|
389
418
|
</script>
|
|
390
419
|
|
|
@@ -918,10 +947,16 @@ $effect(() => {
|
|
|
918
947
|
}
|
|
919
948
|
|
|
920
949
|
/* ─── Resize handles ─────────────────────────────── */
|
|
950
|
+
/* Resizing lives only on the centered grip column. The old full-width
|
|
951
|
+
edge bands (24px of inward slop each) covered short events entirely —
|
|
952
|
+
min block height is 24px, so any tap near an edge risked starting a
|
|
953
|
+
resize. The grip is persistently visible on touch (hover:none below),
|
|
954
|
+
so the smaller target stays discoverable; taps elsewhere open. */
|
|
921
955
|
.mb-ev-handle {
|
|
922
956
|
position: absolute;
|
|
923
|
-
left:
|
|
924
|
-
|
|
957
|
+
left: 50%;
|
|
958
|
+
transform: translateX(-50%);
|
|
959
|
+
width: 56px;
|
|
925
960
|
height: 10px;
|
|
926
961
|
z-index: 2;
|
|
927
962
|
cursor: ns-resize;
|
|
@@ -958,6 +993,8 @@ $effect(() => {
|
|
|
958
993
|
.mb-event:focus-within .mb-ev-handle::after,
|
|
959
994
|
.mb-event--resizing .mb-ev-handle::after,
|
|
960
995
|
.mb-event--selected .mb-ev-handle::after { opacity: 0.55; }
|
|
996
|
+
/* Finger on the grip column itself: brighten so the hit zone reads */
|
|
997
|
+
.mb-ev-handle:hover::after { opacity: 0.9; }
|
|
961
998
|
/* Touch devices have no hover — show the handles persistently. */
|
|
962
999
|
@media (hover: none) {
|
|
963
1000
|
.mb-ev-handle::after { opacity: 0.55; }
|