detached e81bc01

feat(ui): Optimize draggable and window positioning with performance improvements

commit
e81bc01444512a4ab0ff72e159ed97edd709c070
author
Kit · · 2025-02-18 16:28 UTC
parent
ea6754c
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
2 files · +56 -40
M src/lib/actions/draggable.ts +45 -39
@@ -29,34 +29,42 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib
2929 return;
3030 }
3131
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') {
32+ // Ensure proper positioning
33+ if (getComputedStyle(node).position === 'static') {
4334 node.style.position = 'fixed';
4435 }
4536
46- node.style.left = `${x}px`;
47- node.style.top = `${y}px`;
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+ }
4851
4952 function onMouseDown(event: MouseEvent) {
50- if (!isEnabled) return;
53+ if (!isEnabled || event.button !== 0) return;
5154
52- // Only handle left mouse button
53- if (event.button !== 0) return;
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+ }
5460
5561 isDragging = true;
56- const rect = node.getBoundingClientRect();
5762
58- x = rect.left;
59- y = rect.top;
63+ // Get current position
64+ const pos = getNodePosition();
65+ x = pos.x;
66+ y = pos.y;
67+
6068 startX = event.clientX - x;
6169 startY = event.clientY - y;
6270
@@ -64,7 +72,9 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib
6472 detail: { x, y }
6573 }));
6674
67- // Prevent text selection while dragging
75+ // Add dragging class for styling
76+ node.classList.add('dragging');
77+
6878 event.preventDefault();
6979 }
7080
@@ -75,52 +85,48 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib
7585 let newY = event.clientY - startY;
7686
7787 if (bounds === 'window') {
78- // Keep within window bounds
7988 newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth));
8089 newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight));
8190 } else if (bounds === 'parent' && node.parentElement) {
82- // Keep within parent bounds
8391 const parent = node.parentElement;
8492 newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth));
8593 newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight));
8694 }
8795
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- }));
96+ requestAnimationFrame(() => {
97+ updatePosition(newX, newY);
98+ node.dispatchEvent(new CustomEvent('dragmove', {
99+ detail: { x: newX, y: newY }
100+ }));
101+ });
97102 }
98103
99104 function onMouseUp() {
100105 if (!isDragging) return;
101106 isDragging = false;
102107
108+ node.classList.remove('dragging');
109+
103110 node.dispatchEvent(new CustomEvent('dragend', {
104111 detail: { x, y }
105112 }));
106113 }
107114
108115 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);
116+ handleElement.addEventListener('mousedown', onMouseDown);
117+ window.addEventListener('mousemove', onMouseMove, { passive: true });
118+ window.addEventListener('mouseup', onMouseUp);
112119
113120 return {
114121 update(newParams: DraggableParameters) {
115- const newEnabled = newParams.enabled !== undefined ? newParams.enabled : true;
116- isEnabled = newEnabled;
122+ isEnabled = newParams.enabled !== undefined ? newParams.enabled : true;
117123 handleElement.style.cursor = isEnabled ? 'move' : 'default';
118124 Object.assign(params, newParams);
119125 },
120126 destroy() {
121- handleElement.removeEventListener('mousedown', onMouseDown as EventListener);
122- window.removeEventListener('mousemove', onMouseMove as EventListener);
123- window.removeEventListener('mouseup', onMouseUp as EventListener);
127+ handleElement.removeEventListener('mousedown', onMouseDown);
128+ window.removeEventListener('mousemove', onMouseMove);
129+ window.removeEventListener('mouseup', onMouseUp);
124130 }
125131 };
126132 }; ⏎̸
@@ -29,34 +29,42 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib
29 return;29 return;
30 }30 }
31 31
32- // Set initial position from existing inline styles or computed styles32+ // Ensure proper positioning
33- const computedStyle = getComputedStyle(node);33+ if (getComputedStyle(node).position === 'static') {
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';34 node.style.position = 'fixed';
44 }35 }
45 36
46- node.style.left = `${x}px`;37+ function getNodePosition() {
47- node.style.top = `${y}px`;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+ }
48 51
49 function onMouseDown(event: MouseEvent) {52 function onMouseDown(event: MouseEvent) {
50- if (!isEnabled) return;53+ if (!isEnabled || event.button !== 0) return;
51 54
52- // Only handle left mouse button55+ // Check if we're clicking a button in the title bar
53- if (event.button !== 0) return;56+ const target = event.target as HTMLElement;
57+ if (target.tagName === 'BUTTON' || target.closest('button')) {
58+ return;
59+ }
54 60
55 isDragging = true;61 isDragging = true;
56- const rect = node.getBoundingClientRect();
57 62
58- x = rect.left;63+ // Get current position
59- y = rect.top;64+ const pos = getNodePosition();
65+ x = pos.x;
66+ y = pos.y;
67+
60 startX = event.clientX - x;68 startX = event.clientX - x;
61 startY = event.clientY - y;69 startY = event.clientY - y;
62 70
@@ -64,7 +72,9 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib
64 detail: { x, y }72 detail: { x, y }
65 }));73 }));
66 74
67- // Prevent text selection while dragging75+ // Add dragging class for styling
76+ node.classList.add('dragging');
77+
68 event.preventDefault();78 event.preventDefault();
69 }79 }
70 80
@@ -75,52 +85,48 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib
75 let newY = event.clientY - startY;85 let newY = event.clientY - startY;
76 86
77 if (bounds === 'window') {87 if (bounds === 'window') {
78- // Keep within window bounds
79 newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth));88 newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth));
80 newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight));89 newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight));
81 } else if (bounds === 'parent' && node.parentElement) {90 } else if (bounds === 'parent' && node.parentElement) {
82- // Keep within parent bounds
83 const parent = node.parentElement;91 const parent = node.parentElement;
84 newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth));92 newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth));
85 newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight));93 newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight));
86 }94 }
87 95
88- x = newX;96+ requestAnimationFrame(() => {
89- y = newY;97+ updatePosition(newX, newY);
90- 98+ node.dispatchEvent(new CustomEvent('dragmove', {
91- node.style.left = `${x}px`;99+ detail: { x: newX, y: newY }
92- node.style.top = `${y}px`;100+ }));
93- 101+ });
94- node.dispatchEvent(new CustomEvent('dragmove', {
95- detail: { x, y }
96- }));
97 }102 }
98 103
99 function onMouseUp() {104 function onMouseUp() {
100 if (!isDragging) return;105 if (!isDragging) return;
101 isDragging = false;106 isDragging = false;
102 107
108+ node.classList.remove('dragging');
109+
103 node.dispatchEvent(new CustomEvent('dragend', {110 node.dispatchEvent(new CustomEvent('dragend', {
104 detail: { x, y }111 detail: { x, y }
105 }));112 }));
106 }113 }
107 114
108 handleElement.style.cursor = isEnabled ? 'move' : 'default';115 handleElement.style.cursor = isEnabled ? 'move' : 'default';
109- handleElement.addEventListener('mousedown', onMouseDown as EventListener);116+ handleElement.addEventListener('mousedown', onMouseDown);
110- window.addEventListener('mousemove', onMouseMove as EventListener);117+ window.addEventListener('mousemove', onMouseMove, { passive: true });
111- window.addEventListener('mouseup', onMouseUp as EventListener);118+ window.addEventListener('mouseup', onMouseUp);
112 119
113 return {120 return {
114 update(newParams: DraggableParameters) {121 update(newParams: DraggableParameters) {
115- const newEnabled = newParams.enabled !== undefined ? newParams.enabled : true;122+ isEnabled = newParams.enabled !== undefined ? newParams.enabled : true;
116- isEnabled = newEnabled;
117 handleElement.style.cursor = isEnabled ? 'move' : 'default';123 handleElement.style.cursor = isEnabled ? 'move' : 'default';
118 Object.assign(params, newParams);124 Object.assign(params, newParams);
119 },125 },
120 destroy() {126 destroy() {
121- handleElement.removeEventListener('mousedown', onMouseDown as EventListener);127+ handleElement.removeEventListener('mousedown', onMouseDown);
122- window.removeEventListener('mousemove', onMouseMove as EventListener);128+ window.removeEventListener('mousemove', onMouseMove);
123- window.removeEventListener('mouseup', onMouseUp as EventListener);129+ window.removeEventListener('mouseup', onMouseUp);
124 }130 }
125 };131 };
126 }; ⏎̸132 }; ⏎̸
M src/lib/components/chat-room.svelte +11 -1
@@ -640,17 +640,27 @@ function handleLoginSuccess() {
640640 .title-bar {
641641 height: 2rem;
642642 position: relative;
643- cursor: move;
643+ cursor: default;
644644 user-select: none;
645645 display: flex;
646646 align-items: center;
647647 padding: 0 0.5rem;
648648 }
649649
650+ .title-bar:not(:has(button:hover)) {
651+ cursor: move;
652+ }
653+
650654 .chat-window {
651655 position: fixed;
652656 box-sizing: border-box;
653657 transition: width 0.3s ease, height 0.3s ease, left 0.3s ease, top 0.3s ease;
658+ will-change: transform;
659+ }
660+
661+ .chat-window.dragging {
662+ transition: none !important;
663+ user-select: none;
654664 }
655665
656666 .chat-area {
@@ -640,17 +640,27 @@ function handleLoginSuccess() {
640 .title-bar {640 .title-bar {
641 height: 2rem;641 height: 2rem;
642 position: relative;642 position: relative;
643- cursor: move;643+ cursor: default;
644 user-select: none;644 user-select: none;
645 display: flex;645 display: flex;
646 align-items: center;646 align-items: center;
647 padding: 0 0.5rem;647 padding: 0 0.5rem;
648 }648 }
649 649
650+ .title-bar:not(:has(button:hover)) {
651+ cursor: move;
652+ }
653+
650 .chat-window {654 .chat-window {
651 position: fixed;655 position: fixed;
652 box-sizing: border-box;656 box-sizing: border-box;
653 transition: width 0.3s ease, height 0.3s ease, left 0.3s ease, top 0.3s ease;657 transition: width 0.3s ease, height 0.3s ease, left 0.3s ease, top 0.3s ease;
658+ will-change: transform;
659+ }
660+
661+ .chat-window.dragging {
662+ transition: none !important;
663+ user-select: none;
654 }664 }
655 665
656 .chat-area {666 .chat-area {

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