v0.4.0 f6d8405

draggable.ts RawHistoryFolder

1import type { Action } from 'svelte/action';
2
3interface DraggableParameters {
4	handle?: string;
5	bounds?: 'parent' | 'window';
6	enabled?: boolean;
7}
8
9interface DraggableAttributes {
10	ondragstart?: (e: CustomEvent<{ x: number; y: number }>) => void;
11	ondragmove?: (e: CustomEvent<{ x: number; y: number }>) => void;
12	ondragend?: (e: CustomEvent<{ x: number; y: number }>) => void;
13}
14
15export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttributes> = (
16	node,
17	params = {}
18) => {
19	let x = 0;
20	let y = 0;
21	let startX = 0;
22	let startY = 0;
23	let isDragging = false;
24
25	let isEnabled = params.enabled !== undefined ? params.enabled : true;
26	const handleSelector = params.handle ?? null;
27	const bounds = params.bounds ?? 'window';
28
29	const handleElement = handleSelector ? node.querySelector<HTMLElement>(handleSelector) : node;
30	if (!handleElement) {
31		console.warn('Draggable handle element not found');
32		return;
33	}
34
35	// Ensure proper positioning
36	if (getComputedStyle(node).position === 'static') {
37		node.style.position = 'fixed';
38	}
39
40	function getNodePosition() {
41		const rect = node.getBoundingClientRect();
42		return {
43			x: rect.left,
44			y: rect.top
45		};
46	}
47
48	function updatePosition(newX: number, newY: number) {
49		x = newX;
50		y = newY;
51		node.style.left = `${x}px`;
52		node.style.top = `${y}px`;
53	}
54
55	function onMouseDown(event: MouseEvent) {
56		if (!isEnabled || event.button !== 0) return;
57
58		// Check if we're clicking a button in the title bar
59		const target = event.target as HTMLElement;
60		if (target.tagName === 'BUTTON' || target.closest('button')) {
61			return;
62		}
63
64		isDragging = true;
65
66		// Get current position
67		const pos = getNodePosition();
68		x = pos.x;
69		y = pos.y;
70
71		startX = event.clientX - x;
72		startY = event.clientY - y;
73
74		node.dispatchEvent(
75			new CustomEvent('dragstart', {
76				detail: { x, y }
77			})
78		);
79
80		// Add dragging class for styling
81		node.classList.add('dragging');
82
83		event.preventDefault();
84	}
85
86	function onMouseMove(event: MouseEvent) {
87		if (!isDragging || !isEnabled) return;
88
89		let newX = event.clientX - startX;
90		let newY = event.clientY - startY;
91
92		if (bounds === 'window') {
93			newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth));
94			newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight));
95		} else if (bounds === 'parent' && node.parentElement) {
96			const parent = node.parentElement;
97			newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth));
98			newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight));
99		}
100
101		requestAnimationFrame(() => {
102			updatePosition(newX, newY);
103			node.dispatchEvent(
104				new CustomEvent('dragmove', {
105					detail: { x: newX, y: newY }
106				})
107			);
108		});
109	}
110
111	function onMouseUp() {
112		if (!isDragging) return;
113		isDragging = false;
114
115		node.classList.remove('dragging');
116
117		node.dispatchEvent(
118			new CustomEvent('dragend', {
119				detail: { x, y }
120			})
121		);
122	}
123
124	handleElement.style.cursor = isEnabled ? 'move' : 'default';
125	handleElement.addEventListener('mousedown', onMouseDown);
126	window.addEventListener('mousemove', onMouseMove, { passive: true });
127	window.addEventListener('mouseup', onMouseUp);
128
129	return {
130		update(newParams: DraggableParameters) {
131			isEnabled = newParams.enabled !== undefined ? newParams.enabled : true;
132			handleElement.style.cursor = isEnabled ? 'move' : 'default';
133			Object.assign(params, newParams);
134		},
135		destroy() {
136			handleElement.removeEventListener('mousedown', onMouseDown);
137			window.removeEventListener('mousemove', onMouseMove);
138			window.removeEventListener('mouseup', onMouseUp);
139		}
140	};
141};

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close