Skip to content

Commit cee2f0a

Browse files
authored
Merge pull request #285 from DHBern/279-fasskomm-modal-should-be-movabledraggable
feat: fasskomm modal is draggable
2 parents 87d376d + 1b49f0d commit cee2f0a

1 file changed

Lines changed: 99 additions & 2 deletions

File tree

src/routes/fassungen/[thirties=thirties]/FassungskommentarModal.svelte

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,116 @@
22
import { Modal } from '@skeletonlabs/skeleton-svelte';
33
44
let { commentary, id, openState = $bindable() } = $props();
5+
6+
/** @type {HTMLElement | null} */
7+
let headerEl = $state(null);
8+
let offsetX = $state(0);
9+
let offsetY = $state(0);
10+
let dragging = $state(false);
11+
12+
let startX = 0;
13+
let startY = 0;
14+
let startOffsetX = 0;
15+
let startOffsetY = 0;
16+
17+
/** Apply current offset to the Skeleton content wrapper (header's parent). */
18+
$effect(() => {
19+
const contentEl = headerEl?.parentElement;
20+
if (!contentEl) return;
21+
contentEl.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
22+
});
23+
24+
/**
25+
* Clamp a candidate {x, y} so the modal stays grabbable on screen.
26+
* Keeps the header visible (at least HEADER_KEEP px tall) and the modal
27+
* within the viewport with EDGE_MARGIN px of slack.
28+
* @param {number} x
29+
* @param {number} y
30+
* @returns {{ x: number, y: number }}
31+
*/
32+
function clamp(x, y) {
33+
const contentEl = headerEl?.parentElement;
34+
if (!contentEl) return { x, y };
35+
// Measure without our current transform so we get the un-translated rect.
36+
const prevTransform = contentEl.style.transform;
37+
contentEl.style.transform = '';
38+
const rect = contentEl.getBoundingClientRect();
39+
contentEl.style.transform = prevTransform;
40+
41+
const EDGE_MARGIN = 8;
42+
const HEADER_KEEP = headerEl?.offsetHeight ?? 56;
43+
44+
const minX = EDGE_MARGIN - rect.right;
45+
const maxX = window.innerWidth - EDGE_MARGIN - rect.left;
46+
const minY = EDGE_MARGIN - rect.top;
47+
const maxY = window.innerHeight - HEADER_KEEP - rect.top;
48+
49+
return {
50+
x: Math.min(Math.max(x, minX), maxX),
51+
y: Math.min(Math.max(y, minY), maxY)
52+
};
53+
}
54+
55+
/** @param {PointerEvent} e */
56+
function onPointerDown(e) {
57+
// Only primary button / single touch contact
58+
if (e.button !== undefined && e.button !== 0) return;
59+
dragging = true;
60+
startX = e.clientX;
61+
startY = e.clientY;
62+
startOffsetX = offsetX;
63+
startOffsetY = offsetY;
64+
/** @type {HTMLElement} */ (e.currentTarget).setPointerCapture(e.pointerId);
65+
e.preventDefault();
66+
}
67+
68+
/** @param {PointerEvent} e */
69+
function onPointerMove(e) {
70+
if (!dragging) return;
71+
const next = clamp(startOffsetX + (e.clientX - startX), startOffsetY + (e.clientY - startY));
72+
offsetX = next.x;
73+
offsetY = next.y;
74+
}
75+
76+
/** @param {PointerEvent} e */
77+
function onPointerUp(e) {
78+
if (!dragging) return;
79+
dragging = false;
80+
/** @type {HTMLElement} */ (e.currentTarget).releasePointerCapture(e.pointerId);
81+
}
82+
83+
/** Re-clamp on viewport resize so the modal stays on screen. */
84+
function onResize() {
85+
const next = clamp(offsetX, offsetY);
86+
offsetX = next.x;
87+
offsetY = next.y;
88+
}
589
</script>
690
91+
<svelte:window onresize={onResize} />
92+
793
<Modal
894
defaultOpen={true}
995
onOpenChange={() => (openState = false)}
1096
classes="fassungskommentar_modal"
1197
triggerBase="btn preset-tonal"
1298
contentBase="w-[80vw] card preset-filled border border-gray-400 rounded-md text-black space-y-4 shadow-xl max-w-screen-lg"
1399
base=""
14-
backdropClasses="bg-black/[0.03]"
100+
backdropBackground=""
101+
backdropClasses=""
15102
>
16103
{#snippet content()}
17-
<header class="flex items-center justify-center px-4 py-4 bg-gray-400 rounded-t-md">
104+
<!-- svelte-ignore a11y_no_static_element_interactions -- header is a drag handle for the modal; not itself an interactive control -->
105+
<header
106+
bind:this={headerEl}
107+
class="flex items-center justify-center px-4 py-4 bg-gray-400 rounded-t-md select-none touch-none {dragging
108+
? 'cursor-grabbing'
109+
: 'cursor-grab'}"
110+
onpointerdown={onPointerDown}
111+
onpointermove={onPointerMove}
112+
onpointerup={onPointerUp}
113+
onpointercancel={onPointerUp}
114+
>
18115
<h1
19116
class={`text-md uppercase tracking-wider ${
20117
id[2] === 'A' ? 'text-red-800' : 'text-green-900'

0 commit comments

Comments
 (0)