v0.1 64c03e0

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> = (node, params = {}) => {
16  let x = 0;
17  let y = 0;
18  let startX = 0;
19  let startY = 0;
20  let isDragging = false;
21  
22  let isEnabled = params.enabled !== undefined ? params.enabled : true;
23  const handleSelector = params.handle ?? null;
24  const bounds = params.bounds ?? 'window';
25  
26  const handleElement = handleSelector ? node.querySelector<HTMLElement>(handleSelector) : node;
27  if (!handleElement) {
28    console.warn('Draggable handle element not found');
29    return;
30  }
31
32  // Set initial position from existing inline styles or computed styles
33  const computedStyle = getComputedStyle(node);
34  const existingLeft = node.style.left || computedStyle.left;
35  const existingTop = node.style.top || computedStyle.top;
36  
37  x = parseInt(existingLeft) || 0;
38  y = parseInt(existingTop) || 0;
39
40  // Ensure the element is positioned
41  const position = computedStyle.position;
42  if (position !== 'relative' && position !== 'absolute' && position !== 'fixed') {
43    node.style.position = 'fixed';
44  }
45
46  node.style.left = `${x}px`;
47  node.style.top = `${y}px`;
48
49  function onMouseDown(event: MouseEvent) {
50    if (!isEnabled) return;
51    
52    // Only handle left mouse button
53    if (event.button !== 0) return;
54    
55    isDragging = true;
56    const rect = node.getBoundingClientRect();
57    
58    x = rect.left;
59    y = rect.top;
60    startX = event.clientX - x;
61    startY = event.clientY - y;
62    
63    node.dispatchEvent(new CustomEvent('dragstart', {
64      detail: { x, y }
65    }));
66    
67    // Prevent text selection while dragging
68    event.preventDefault();
69  }
70
71  function onMouseMove(event: MouseEvent) {
72    if (!isDragging || !isEnabled) return;
73
74    let newX = event.clientX - startX;
75    let newY = event.clientY - startY;
76
77    if (bounds === 'window') {
78      // Keep within window bounds
79      newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth));
80      newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight));
81    } else if (bounds === 'parent' && node.parentElement) {
82      // Keep within parent bounds
83      const parent = node.parentElement;
84      newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth));
85      newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight));
86    }
87
88    x = newX;
89    y = newY;
90    
91    node.style.left = `${x}px`;
92    node.style.top = `${y}px`;
93    
94    node.dispatchEvent(new CustomEvent('dragmove', {
95      detail: { x, y }
96    }));
97  }
98
99  function onMouseUp() {
100    if (!isDragging) return;
101    isDragging = false;
102    
103    node.dispatchEvent(new CustomEvent('dragend', {
104      detail: { x, y }
105    }));
106  }
107
108  handleElement.style.cursor = isEnabled ? 'move' : 'default';
109  handleElement.addEventListener('mousedown', onMouseDown as EventListener);
110  window.addEventListener('mousemove', onMouseMove as EventListener);
111  window.addEventListener('mouseup', onMouseUp as EventListener);
112
113  return {
114    update(newParams: DraggableParameters) {
115      const newEnabled = newParams.enabled !== undefined ? newParams.enabled : true;
116      isEnabled = newEnabled;
117      handleElement.style.cursor = isEnabled ? 'move' : 'default';
118      Object.assign(params, newParams);
119    },
120    destroy() {
121      handleElement.removeEventListener('mousedown', onMouseDown as EventListener);
122      window.removeEventListener('mousemove', onMouseMove as EventListener);
123      window.removeEventListener('mouseup', onMouseUp as EventListener);
124    }
125  };
126}; 

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