From 3b7a6271ac8d03c69aa4f60289ad4fb9088ced89 Mon Sep 17 00:00:00 2001 From: iRedTea Date: Fri, 16 Oct 2026 10:21:37 +0300 Subject: [PATCH] feat: a dragged tab held over another tab's title opens it to be dropped into; let go with nothing done, the dragged tab comes back --- internal/server/web/static/workspace.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/server/web/static/workspace.js b/internal/server/web/static/workspace.js index 797eb49..b6db73e 100644 --- a/internal/server/web/static/workspace.js +++ b/internal/server/web/static/workspace.js @@ -348,6 +348,8 @@ } function beginDrag() { drag.on = true; + // The tab open when the drag began: it comes back if nothing is done. + drag.origActive = active; const src = drag.kind === 'tab' ? drag.obj.el : drag.obj.el.querySelector('.pane-head'); const g = document.createElement('div'); g.className = 'drag-ghost ' + drag.kind; @@ -390,6 +392,16 @@ const stripShown = document.body.classList.contains('multi') || document.body.classList.contains('multi-hint'); const inStrip = stripShown && ev.clientY >= strip.top - 6 && ev.clientY <= strip.bottom + 10; const inArea = active && (() => { const r = active.view.getBoundingClientRect(); return ev.clientX >= r.left && ev.clientX <= r.right && ev.clientY >= r.top && ev.clientY <= r.bottom; })(); + // A tab held over another tab's title for a moment opens that one, so + // the dragged tab can be dropped into it. + if (drag.kind === 'tab') { + const over = inStrip ? tabs.find(t => t !== drag.obj && t !== active && (() => { const r = t.el.getBoundingClientRect(); return ev.clientX >= r.left && ev.clientX <= r.right; })()) : null; + if (over !== drag.overTab) { + clearTimeout(drag.overTimer); + drag.overTab = over; + if (over) drag.overTimer = setTimeout(() => { if (drag && drag.overTab === over) activate(over); }, 450); + } + } if (inStrip) { const s = stripIndex(ev.clientX); mark.hidden = false; mark.style.left = s.mx + 'px'; @@ -425,6 +437,14 @@ const d = drag; drag = null; if (!d.on) return; + clearTimeout(d.overTimer); + // Dropped with nothing done (or only moved along the strip): back to the + // tab that was dragged, if another one was opened meanwhile. + const tg0 = d.target; + if (d.kind === 'tab' && d.origActive && active !== d.origActive && !(tg0 && tg0.kind === 'merge')) { + const back = d.origActive; + setTimeout(() => activate(back), 0); + } d.ghost.remove(); d.src.classList.remove('dragging'); document.body.classList.remove('dnd', 'multi-hint');