Skip to content

Commit 9f02925

Browse files
committed
feat: drag selected windows on touch
1 parent 7048eca commit 9f02925

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,10 @@
446446
.card.chart .plot.can-select-window {
447447
cursor: pointer;
448448
}
449+
.card.chart .plot,
450+
.card.strip {
451+
touch-action: pan-y;
452+
}
449453
/* Fixed so the strip's height never depends on whether its svg is present.
450454
Must match the height drawTrips() computes: PAD.t + 2 + barH + 26. */
451455
.card.strip .plot {
@@ -507,6 +511,17 @@
507511
font-size: 10.5px;
508512
white-space: normal;
509513
}
514+
.tt .touch-help {
515+
display: none;
516+
}
517+
@media (pointer: coarse) {
518+
.tt .keyboard-help {
519+
display: none;
520+
}
521+
.tt .touch-help {
522+
display: block;
523+
}
524+
}
510525

511526
.scrolly {
512527
overflow-y: auto;

src/app.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,8 @@ interface Domain {
345345
`<span class="muted">${over ? `${out - LIMIT} over the ${LIMIT}-day limit`
346346
: `${LIMIT - out} days of headroom`}</span>` +
347347
(selected
348-
? `<hr><span class="selection-help">ESC to dismiss selection; arrow keys or H/L adjust by day; hold Shift to adjust by week.</span>`
348+
? `<hr><span class="selection-help keyboard-help">ESC to dismiss selection; arrow keys or H/L adjust by day; hold Shift to adjust by week.</span>` +
349+
`<span class="selection-help touch-help">Swipe left or right to adjust the window; tap elsewhere to dismiss.</span>`
349350
: "");
350351
}
351352

@@ -910,6 +911,67 @@ interface Domain {
910911
});
911912
}
912913

914+
function initChartSelectionTouchSliding(): void {
915+
for (const host of [el("plot1"), el("trip-timeline")]) {
916+
let pointerId: number | null = null;
917+
let startX = 0;
918+
let startY = 0;
919+
let startDay = 0;
920+
let startWindowEnd = 0;
921+
let dragging = false;
922+
let suppressClick = false;
923+
924+
host.addEventListener("pointerdown", (ev: PointerEvent) => {
925+
if (ev.pointerType !== "touch" || selectedWindowEnd === null || !rollingHover) return;
926+
pointerId = ev.pointerId;
927+
startX = ev.clientX;
928+
startY = ev.clientY;
929+
startDay = rollingHover.dateAt(ev.clientX);
930+
startWindowEnd = selectedWindowEnd;
931+
dragging = false;
932+
host.setPointerCapture(ev.pointerId);
933+
});
934+
935+
host.addEventListener("pointermove", (ev: PointerEvent) => {
936+
if (ev.pointerId !== pointerId || selectedWindowEnd === null || !rollingHover) return;
937+
const dx = ev.clientX - startX;
938+
const dy = ev.clientY - startY;
939+
if (!dragging) {
940+
if (Math.hypot(dx, dy) < 6) return;
941+
if (Math.abs(dy) >= Math.abs(dx)) {
942+
if (host.hasPointerCapture(ev.pointerId)) host.releasePointerCapture(ev.pointerId);
943+
pointerId = null;
944+
return;
945+
}
946+
dragging = true;
947+
}
948+
949+
ev.preventDefault();
950+
const dayDelta = rollingHover.dateAt(ev.clientX) - startDay;
951+
const targetEnd = rollingHover.clampDate(startWindowEnd + dayDelta);
952+
const days = targetEnd - selectedWindowEnd;
953+
if (days !== 0) moveSelectedWindowBy(days);
954+
});
955+
956+
const finish = (ev: PointerEvent): void => {
957+
if (ev.pointerId !== pointerId) return;
958+
if (host.hasPointerCapture(ev.pointerId)) host.releasePointerCapture(ev.pointerId);
959+
suppressClick = dragging;
960+
pointerId = null;
961+
dragging = false;
962+
if (suppressClick) setTimeout(() => { suppressClick = false; }, 0);
963+
};
964+
host.addEventListener("pointerup", finish);
965+
host.addEventListener("pointercancel", finish);
966+
host.addEventListener("click", (ev: MouseEvent) => {
967+
if (!suppressClick) return;
968+
suppressClick = false;
969+
ev.preventDefault();
970+
ev.stopImmediatePropagation();
971+
}, true);
972+
}
973+
}
974+
913975
function moveSelectedWindowBy(days: number): void {
914976
if (selectedWindowEnd === null || !rollingHover || !rollingTooltip) return;
915977
keepAlive();
@@ -1238,6 +1300,7 @@ interface Domain {
12381300
initChartSelectionKeyboard();
12391301
initChartScrollVisibility();
12401302
initChartSelectionTouchDismissal();
1303+
initChartSelectionTouchSliding();
12411304
loadFromFile();
12421305
if (!restoreSharedData()) restoreData();
12431306
restoreSelectedWindow();

test/app.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ test("rolling chart selection locks hover and supports keyboard control", () =>
4141
assert.match(appSource, /if \(ev\.pointerType !== "touch" \|\| selectedWindowEnd === null\) return;/);
4242
assert.match(appSource, /target\?\.closest\("\.card\.chart, #trip-timeline"\)/);
4343
assert.match(appSource, /initChartSelectionTouchDismissal\(\);/);
44+
assert.match(appSource, /for \(const host of \[el\("plot1"\), el\("trip-timeline"\)\]\)/);
45+
assert.match(appSource, /host\.addEventListener\("pointermove"/);
46+
assert.match(appSource, /const dayDelta = rollingHover\.dateAt\(ev\.clientX\) - startDay;/);
47+
assert.match(appSource, /const days = targetEnd - selectedWindowEnd;\s*if \(days !== 0\) moveSelectedWindowBy\(days\);/);
48+
assert.match(appSource, /ev\.stopImmediatePropagation\(\);/);
49+
assert.match(appSource, /initChartSelectionTouchSliding\(\);/);
4450
assert.match(appSource, /ESC to dismiss selection; arrow keys or H\/L adjust by day; hold Shift to adjust by week\./);
51+
assert.match(appSource, /Swipe left or right to adjust the window; tap elsewhere to dismiss\./);
52+
assert.match(indexSource, /touch-action: pan-y;/);
4553
assert.match(appSource, /rollingHover\.show\(selectedWindowEnd, true\)/);
4654
assert.match(indexSource, /\.tt \.selection-help \{[\s\S]*?white-space: normal;/);
4755
assert.match(appSource, /host\.onwheel = \(ev: WheelEvent\) => moveSelectedWindowFromWheel\(ev, host\);/);

0 commit comments

Comments
 (0)