Drag & drop tab reordering
Tabs are draggable within the strip: HTML5 DnD with the WebKit setData quirk handled, an inset primary insertion line at the drop position (no layout shift), and the dragged tab dimmed. Reordering only touches the workspaces array order; ids, sessions, and the active tab are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -274,6 +274,18 @@ export class PanelsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Reorder a tab (drag & drop). Order is purely visual; ids stay stable. */
|
||||
moveWorkspace(id: number, toIndex: number): void {
|
||||
this.workspaces.update((list) => {
|
||||
const fromIndex = list.findIndex((w) => w.id === id);
|
||||
if (fromIndex < 0) return list;
|
||||
const next = [...list];
|
||||
const [moved] = next.splice(fromIndex, 1);
|
||||
next.splice(Math.max(0, Math.min(next.length, toIndex)), 0, moved);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
cycleWorkspace(delta: 1 | -1): void {
|
||||
const list = this.workspaces();
|
||||
if (list.length < 2) return;
|
||||
|
||||
@@ -26,10 +26,17 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
[class.pl-1.5]="!isMacChrome"
|
||||
[class.pt-1]="!isMacChrome"
|
||||
>
|
||||
@for (ws of panels.workspaces(); track ws.id) {
|
||||
@for (ws of panels.workspaces(); track ws.id; let i = $index) {
|
||||
<div
|
||||
class="group relative flex max-w-52 items-center rounded-md electrobun-webkit-app-region-no-drag"
|
||||
[class.bg-accent]="ws.id === panels.activeWorkspaceId()"
|
||||
[class.opacity-50]="draggedId() === ws.id"
|
||||
[style.box-shadow]="dropIndicatorShadow(i)"
|
||||
draggable="true"
|
||||
(dragstart)="onDragStart($event, ws.id)"
|
||||
(dragover)="onDragOver($event, i)"
|
||||
(drop)="onDrop($event, i)"
|
||||
(dragend)="onDragEnd()"
|
||||
>
|
||||
<button
|
||||
(click)="panels.setActiveWorkspace(ws.id)"
|
||||
@@ -190,8 +197,56 @@ export class TabBar {
|
||||
readonly transferModes = TRANSFER_MODES;
|
||||
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
||||
|
||||
// drag & drop reordering
|
||||
readonly draggedId = signal<number | null>(null);
|
||||
readonly dropTarget = signal<{ index: number; before: boolean } | null>(null);
|
||||
|
||||
close(event: MouseEvent, id: number): void {
|
||||
event.stopPropagation();
|
||||
this.panels.closeWorkspace(id);
|
||||
}
|
||||
|
||||
/** Insertion line at the drop position (inset shadow: no layout shift). */
|
||||
dropIndicatorShadow(index: number): string | null {
|
||||
const target = this.dropTarget();
|
||||
if (!target || target.index !== index || this.draggedId() === null) return null;
|
||||
return target.before
|
||||
? 'inset 2px 0 0 var(--primary)'
|
||||
: 'inset -2px 0 0 var(--primary)';
|
||||
}
|
||||
|
||||
onDragStart(event: DragEvent, id: number): void {
|
||||
this.draggedId.set(id);
|
||||
// WebKit refuses to start a drag without data
|
||||
event.dataTransfer?.setData('text/plain', String(id));
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
onDragOver(event: DragEvent, index: number): void {
|
||||
if (this.draggedId() === null) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
||||
const el = event.currentTarget as HTMLElement;
|
||||
const before = event.clientX < el.getBoundingClientRect().left + el.offsetWidth / 2;
|
||||
this.dropTarget.set({ index, before });
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent, index: number): void {
|
||||
event.preventDefault();
|
||||
const id = this.draggedId();
|
||||
const target = this.dropTarget();
|
||||
this.onDragEnd();
|
||||
if (id === null || !target) return;
|
||||
const list = this.panels.workspaces();
|
||||
const fromIndex = list.findIndex((w) => w.id === id);
|
||||
if (fromIndex < 0) return;
|
||||
let toIndex = target.before ? index : index + 1;
|
||||
if (fromIndex < toIndex) toIndex--; // account for removal before insert
|
||||
if (toIndex !== fromIndex) this.panels.moveWorkspace(id, toIndex);
|
||||
}
|
||||
|
||||
onDragEnd(): void {
|
||||
this.draggedId.set(null);
|
||||
this.dropTarget.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user