v0.1.5 0b43029

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  // Ensure proper positioning
33  if (getComputedStyle(node).position === 'static') {
34    node.style.position = 'fixed';
35  }
36
37  function getNodePosition() {
38    const rect = node.getBoundingClientRect();
39    return {
40      x: rect.left,
41      y: rect.top
42    };
43  }
44
45  function updatePosition(newX: number, newY: number) {
46    x = newX;
47    y = newY;
48    node.style.left = `${x}px`;
49    node.style.top = `${y}px`;
50  }
51
52  function onMouseDown(event: MouseEvent) {
53    if (!isEnabled || event.button !== 0) return;
54    
55    // Check if we're clicking a button in the title bar
56    const target = event.target as HTMLElement;
57    if (target.tagName === 'BUTTON' || target.closest('button')) {
58      return;
59    }
60    
61    isDragging = true;
62    
63    // Get current position
64    const pos = getNodePosition();
65    x = pos.x;
66    y = pos.y;
67    
68    startX = event.clientX - x;
69    startY = event.clientY - y;
70    
71    node.dispatchEvent(new CustomEvent('dragstart', {
72      detail: { x, y }
73    }));
74    
75    // Add dragging class for styling
76    node.classList.add('dragging');
77    
78    event.preventDefault();
79  }
80
81  function onMouseMove(event: MouseEvent) {
82    if (!isDragging || !isEnabled) return;
83
84    let newX = event.clientX - startX;
85    let newY = event.clientY - startY;
86
87    if (bounds === 'window') {
88      newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth));
89      newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight));
90    } else if (bounds === 'parent' && node.parentElement) {
91      const parent = node.parentElement;
92      newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth));
93      newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight));
94    }
95
96    requestAnimationFrame(() => {
97      updatePosition(newX, newY);
98      node.dispatchEvent(new CustomEvent('dragmove', {
99        detail: { x: newX, y: newY }
100      }));
101    });
102  }
103
104  function onMouseUp() {
105    if (!isDragging) return;
106    isDragging = false;
107    
108    node.classList.remove('dragging');
109    
110    node.dispatchEvent(new CustomEvent('dragend', {
111      detail: { x, y }
112    }));
113  }
114
115  handleElement.style.cursor = isEnabled ? 'move' : 'default';
116  handleElement.addEventListener('mousedown', onMouseDown);
117  window.addEventListener('mousemove', onMouseMove, { passive: true });
118  window.addEventListener('mouseup', onMouseUp);
119
120  return {
121    update(newParams: DraggableParameters) {
122      isEnabled = newParams.enabled !== undefined ? newParams.enabled : true;
123      handleElement.style.cursor = isEnabled ? 'move' : 'default';
124      Object.assign(params, newParams);
125    },
126    destroy() {
127      handleElement.removeEventListener('mousedown', onMouseDown);
128      window.removeEventListener('mousemove', onMouseMove);
129      window.removeEventListener('mouseup', onMouseUp);
130    }
131  };
132}; 

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 9

↑↓ moveenter openesc close