Merge public ihasmail: drag calendar events to another day in the week grid (#400)

This commit is contained in:
2026-09-18 21:30:38 -07:00
3 changed files with 130 additions and 43 deletions
@@ -10,6 +10,8 @@ import {
snap,
movePatch,
moveByDaysPatch,
moveAcrossPatch,
columnsMoved,
dayDelta,
resizePatch,
SNAP_MINUTES,
@@ -180,10 +182,22 @@ describe("the patch a drag sends, computed in the event's own frame", () => {
expect(resizePatch(3600, -600)).toEqual({ duration: "PT15M" });
});
it("moves by days and minutes together, as a week-grid drag does", () => {
expect(moveAcrossPatch("2026-09-04T14:00:00", 2, 90)).toEqual({ start: "2026-09-06T15:30:00" });
expect(moveAcrossPatch("2026-09-04T14:00:00", -1, 0)).toEqual({ start: "2026-09-03T14:00:00" });
expect(moveAcrossPatch("2026-09-04T14:00:00", 0, -30)).toEqual({ start: "2026-09-04T13:30:00" });
});
it("adds the days as days, so a clock change does not move the hour", () => {
// US clocks go back on 1 November 2026; 14:00 stays 14:00 across it.
expect(moveAcrossPatch("2026-10-31T14:00:00", 2, 0)).toEqual({ start: "2026-11-02T14:00:00" });
});
it("says nothing at all about a start it cannot read", () => {
expect(movePatch("not a date", 30)).toEqual({});
expect(moveByDaysPatch("", 3)).toEqual({});
expect(moveByDaysPatch("2026-09-04T14:00:00", Number.NaN)).toEqual({});
expect(moveAcrossPatch("not a date", 1, 30)).toEqual({});
});
});
@@ -228,3 +242,23 @@ describe("pixelsToMinutes", () => {
expect(SNAP_MINUTES).toBe(15);
});
});
describe("columnsMoved", () => {
it("counts whole columns, to the nearest", () => {
expect(columnsMoved(100, 100, 2, 7)).toBe(1);
expect(columnsMoved(140, 100, 2, 7)).toBe(1);
expect(columnsMoved(160, 100, 2, 7)).toBe(2);
expect(columnsMoved(-40, 100, 2, 7)).toBe(0);
expect(columnsMoved(-160, 100, 3, 7)).toBe(-2);
});
it("stops at the edges of the week instead of wrapping", () => {
expect(columnsMoved(-900, 100, 2, 7)).toBe(-2);
expect(columnsMoved(900, 100, 2, 7)).toBe(4);
});
it("never moves sideways in a one-day grid or before it is measured", () => {
expect(columnsMoved(500, 100, 0, 1)).toBe(0);
expect(columnsMoved(500, 0, 0, 7)).toBe(0);
});
});
+29
View File
@@ -142,6 +142,35 @@ export function moveByDaysPatch(storedStart: string, days: number): DragPatch {
return { start: formatStored(moved) };
}
/**
* Moved by whole days and by minutes at once -- the week grid, where a drag
* goes sideways to another day and up or down to another hour in the same
* gesture.
*
* The days go first and as days, for the reason moveByDaysPatch gives: a day
* added to a wall clock keeps its time of day across a clock change, where
* 1440 minutes would not.
*/
export function moveAcrossPatch(storedStart: string, days: number, deltaMinutes: number): DragPatch {
const byDays = days ? moveByDaysPatch(storedStart, days).start : storedStart;
if (!byDays) return {};
return snap(deltaMinutes) ? movePatch(byDays, deltaMinutes) : { start: byDays };
}
/**
* How many columns sideways the pointer has gone, kept inside the grid.
*
* Counted from the column the drag began in, so an event that crosses
* midnight moves by the same amount whichever of its two halves was picked
* up. Past the first or last column it stops at the edge rather than
* wrapping: the week on screen is the only week a drag can reach.
*/
export function columnsMoved(deltaPixels: number, columnWidth: number, fromIndex: number, columnCount: number): number {
if (!columnWidth || columnCount < 2) return 0;
const moved = Math.round(deltaPixels / columnWidth) || 0; // never -0
return Math.max(-fromIndex, Math.min(columnCount - 1 - fromIndex, moved));
}
/** Whole days between two local dates, ignoring the time of day on each. */
export function dayDelta(from: Date, to: Date): number {
const a = new Date(from.getFullYear(), from.getMonth(), from.getDate()).getTime();