diff --git a/sections/workspace.js b/sections/workspace.js index e4d7d8d..138c32f 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -618,672 +618,738 @@ export const workspacesSection = { const contentDiv = parseElement(`
`); const pinnedTabsContainer = parseElement(`
`); const regularTabsContainer = parseElement(`
`); - allTabs - .filter( - (tabEl) => - tabEl && - tabEl.getAttribute("zen-workspace-id") === uuid && - !tabEl.hasAttribute("zen-essential"), - ) - .forEach((tabEl) => { - // Proxy: show tab info with icon, title, and copy-link - const tabUrl = tabEl.linkedBrowser?.currentURI?.spec || tabEl.getAttribute('data-url') || tabEl.getAttribute('label') || ''; - const tabTitle = tabEl.getAttribute('label') || tabEl.getAttribute('title') || 'Tab'; - let faviconUrl = tabEl.getAttribute('image') || tabEl.getAttribute('icon') || ''; - if (!faviconUrl && tabUrl.startsWith('http')) { - faviconUrl = `https://www.google.com/s2/favicons?sz=32&domain_url=${encodeURIComponent(tabUrl)}`; - } - // HTML part (tabProxy) - const tabProxy = parseElement(` -
- - ${faviconUrl ? `` : ''} - - ${tabTitle} -
- `); - // XUL part (copy-link button) - appendXUL( - tabProxy, - ``, - null, - true - ); - tabProxy.addEventListener('contextmenu', (e) => { - e.preventDefault(); - if (tabEl && typeof tabEl.dispatchEvent === 'function') { - const evt = new MouseEvent('contextmenu', { - bubbles: true, - cancelable: true, - view: window, - clientX: e.clientX, - clientY: e.clientY, - screenX: e.screenX, - screenY: e.screenY, - button: 2 - }); - tabEl.dispatchEvent(evt); - } - }); - tabProxy.querySelector('.copy-link').addEventListener('click', (e) => { - e.stopPropagation(); - console.log(`[ZenHaven] Copying URL for tab: ${tabTitle} (${tabUrl})`); - if (tabUrl) { - // Try modern clipboard API first - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(tabUrl).then(() => { - if (typeof gZenUIManager !== 'undefined' && gZenUIManager.showToast) { - gZenUIManager.showToast('zen-copy-current-url-confirmation'); - } - }).catch(() => { - // Fallback if clipboard API fails - console.error(`[ZenHaven] Clipboard API failed, falling back to execCommand for tab: ${tabTitle} (${tabUrl})`); + const processChildren = (children, targetContainer) => { + for (const child of children) { + if (child.matches('tab-group')) { + const groupEl = child; + const groupName = groupEl.getAttribute('label') || 'Tab Group'; + const groupColor = groupEl.getAttribute('color'); // e.g. "red" + + const groupProxy = parseElement(` +
+
+ + + + ${groupName} +
+
+
+ `); + + const groupHeader = groupProxy.querySelector('.haven-tab-group-header'); + groupHeader.addEventListener('click', (e) => { + if (window.getSelection().toString()) { + return; + } + e.stopPropagation(); + groupProxy.classList.toggle('collapsed'); }); - } else { - console.error(`[ZenHaven] Clipboard API failed, falling back to execCommand for tab: ${tabTitle} (${tabUrl})`); - } - } - }); - // --- Drag-and-drop logic for tabs --- - tabProxy.tabEl = tabEl; // Attach real tab reference - tabProxy.dataset.tabId = tabEl.getAttribute('id') || ''; - tabProxy.dataset.pinned = tabEl.hasAttribute('pinned') ? 'true' : 'false'; - tabProxy.dataset.workspaceUuid = uuid; - // Track original parent and index for restoration - let originalTabParent = null; - let originalTabIndex = null; - tabProxy.addEventListener('dragstart', (e) => { - tabProxy.classList.add('dragging'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', tabProxy.dataset.tabId); - tabProxy.parentNode.classList.add('drag-source'); - contentDiv.classList.add('tab-drag-context'); - // Save original parent and index - originalTabParent = tabProxy.parentNode; - originalTabIndex = Array.from(tabProxy.parentNode.children).indexOf(tabProxy); - }); - tabProxy.addEventListener('dragend', (e) => { - tabProxy.classList.remove('dragging'); - document.querySelectorAll('.haven-workspace-pinned-tabs, .haven-workspace-regular-tabs').forEach(c => c.classList.remove('drag-over', 'drag-source')); - contentDiv.classList.remove('tab-drag-context'); - // If not in a valid container, restore to original position - const validContainers = [pinnedTabsContainer, regularTabsContainer]; - if (!validContainers.includes(tabProxy.parentNode)) { - if (originalTabParent && originalTabIndex !== null) { - const children = Array.from(originalTabParent.children); - if (children.length > originalTabIndex) { - originalTabParent.insertBefore(tabProxy, children[originalTabIndex]); - } else { - originalTabParent.appendChild(tabProxy); - } - } - } - originalTabParent = null; - originalTabIndex = null; - }); - // Prevent default drop on document/body to avoid accidental drops outside - if (!window.__zenTabDropPrevented) { - window.addEventListener('dragover', e => e.preventDefault()); - window.addEventListener('drop', e => e.preventDefault()); - window.__zenTabDropPrevented = true; - } - // Only allow drop on containers within this workspace - [pinnedTabsContainer, regularTabsContainer].forEach(container => { - container.addEventListener('dragover', (e) => { - // Only allow drop if this workspace is the drag context - if (!contentDiv.classList.contains('tab-drag-context')) return; - e.preventDefault(); - container.classList.add('drag-over'); - }); - container.addEventListener('dragleave', (e) => { - container.classList.remove('drag-over'); - }); - container.addEventListener('drop', async (e) => { - // Only allow drop if this workspace is the drag context - if (!contentDiv.classList.contains('tab-drag-context')) return; - e.preventDefault(); - container.classList.remove('drag-over'); - const dragging = container.querySelector('.dragging') || document.querySelector('.haven-tab.dragging'); - if (!dragging) return; - // Only allow drop if the tab belongs to this workspace - if (dragging.dataset.workspaceUuid !== uuid) return; - // Always use vertical position to determine drop location - const after = getTabAfterElement(container, e.clientY); - if (after == null) { - container.appendChild(dragging); - } else { - container.insertBefore(dragging, after); - } - // Update pin state if moved between containers - const isPinnedTarget = container === pinnedTabsContainer; - const tabEl = dragging.tabEl; - if (tabEl) { - if (isPinnedTarget) { - tabEl.setAttribute('pinned', 'true'); - } else { - tabEl.removeAttribute('pinned'); - } - } - // Update order in gZenWorkspaces - if (typeof gZenWorkspaces?.reorderTab === 'function') { - const newIndex = Array.from(container.children).indexOf(dragging); - await gZenWorkspaces.reorderTab(tabEl, newIndex, isPinnedTarget); - } - }); - }); - // Helper for reordering within container - function getTabAfterElement(container, y) { - const draggableTabs = [...container.querySelectorAll('.haven-tab:not(.dragging)')]; - return draggableTabs.reduce((closest, child) => { - const box = child.getBoundingClientRect(); - const offset = y - box.top - box.height / 2; - if (offset < 0 && offset > closest.offset) { - return { offset: offset, element: child }; - } else { - return closest; - } - }, { offset: Number.NEGATIVE_INFINITY }).element; - } - // --- End drag-and-drop logic --- - // --- Custom drag-and-drop logic for vertical tabs --- - function getAllTabProxies() { - return [ - ...pinnedTabsContainer.querySelectorAll('.haven-tab'), - ...regularTabsContainer.querySelectorAll('.haven-tab'), - ]; - } - - let isDragging = false; - let dragTab = null; - let dragStartY = 0; - let dragStartX = 0; - let dragOffsetY = 0; - let dragOffsetX = 0; - let dragMouseOffset = 0; - let placeholder = null; - let lastContainer = null; - let dragHoldTimeout = null; - - // --- Drag-and-drop logic for tabProxy --- - tabProxy.addEventListener('mousedown', async (e) => { - if (e.button !== 0) return; // Only left click - if (e.target.closest('.copy-link')) return; // Don't start drag on copy-link button - const isPinned = tabEl && tabEl.hasAttribute('pinned'); - - // Check if workspace is active for pinned tabs - if (isPinned) { - const workspaceEl = gZenWorkspaces.workspaceElement(uuid); - if (!workspaceEl || !workspaceEl.hasAttribute('active')) { - if (window.gZenUIManager && gZenUIManager.showToast) { - gZenUIManager.showToast('Pinned tab order can only be changed in the active workspace.'); - } else { - alert('Pinned tab order can only be changed in the active workspace.'); - } - return; - } - } - - if (tabEl && !tabEl.hasAttribute('id')) { - tabEl.setAttribute('id', 'zen-real-tab-' + Math.random().toString(36).slice(2)); - } - e.preventDefault(); - let dragStarted = false; - dragHoldTimeout = setTimeout(() => { - dragStarted = true; - isDragging = true; - dragTab = tabProxy; - const tabRect = tabProxy.getBoundingClientRect(); - dragStartY = tabRect.top; - dragStartX = tabRect.left; - dragMouseOffset = e.clientY - tabRect.top; - dragOffsetY = 0; - dragOffsetX = e.clientX - tabRect.left; - lastContainer = tabProxy.parentNode; - dragTab._dragSection = isPinned ? 'pinned' : 'regular'; - // --- Insert placeholder BEFORE moving tab out of DOM --- - placeholder = document.createElement('div'); - placeholder.className = 'haven-tab drag-placeholder'; - placeholder.style.height = `${tabProxy.offsetHeight}px`; - placeholder.style.width = `${tabProxy.offsetWidth}px`; - tabProxy.parentNode.insertBefore(placeholder, tabProxy); - // --- Now move tab out of flow: fixed position at its original screen position --- - tabProxy.style.position = 'fixed'; - tabProxy.style.top = `${tabRect.top}px`; - tabProxy.style.left = `${tabRect.left}px`; - tabProxy.style.width = `${tabRect.width}px`; - tabProxy.style.height = `${tabRect.height}px`; - tabProxy.style.zIndex = 1000; - tabProxy.style.pointerEvents = 'none'; - tabProxy.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; - tabProxy.style.transform = 'scale(0.92)'; - tabProxy.setAttribute('drag-tab', ''); - tabProxy.classList.add('dragging-tab'); - document.body.appendChild(tabProxy); - document.body.style.userSelect = 'none'; - getAllWorkspaces().forEach(ws => { - ws.querySelectorAll('.haven-tab').forEach(tab => { - if (tab !== dragTab) { - tab.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; + + const groupTabsContainer = groupProxy.querySelector('.haven-tab-group-tabs'); + + for (const tabEl of groupEl.tabs) { + if (tabEl.hasAttribute('zen-essential')) continue; + // Create a simple, non-draggable proxy for tabs inside a group + const tabUrl = tabEl.linkedBrowser?.currentURI?.spec || tabEl.getAttribute('data-url') || tabEl.getAttribute('label') || ''; + const tabTitle = tabEl.getAttribute('label') || tabEl.getAttribute('title') || 'Tab'; + let faviconUrl = tabEl.getAttribute('image') || tabEl.getAttribute('icon') || ''; + if (!faviconUrl && tabUrl.startsWith('http')) { + faviconUrl = `https://www.google.com/s2/favicons?sz=32&domain_url=${encodeURIComponent(tabUrl)}`; } - }); - }); - window.addEventListener('mousemove', onDragMove); - window.addEventListener('mouseup', onDragEnd); - }, 500); - // If mouse is released before 0.5s, cancel drag - function cancelHold(e2) { - clearTimeout(dragHoldTimeout); - window.removeEventListener('mouseup', cancelHold); - window.removeEventListener('mouseleave', cancelHold); - } - window.addEventListener('mouseup', cancelHold); - window.addEventListener('mouseleave', cancelHold); - }); - - // Helper: Sync the custom UI with the real Firefox tab order - function syncCustomUIWithRealTabs() { - // For the active workspace only - const workspaceEl = gZenWorkspaces.workspaceElement(uuid); - if (!workspaceEl) return; - // Pinned - const pinnedContainer = workspaceEl.querySelector(".haven-workspace-pinned-tabs"); - const realPinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned && t.getAttribute('zen-workspace-id') === uuid); - realPinnedTabs.forEach(tab => { - const proxy = Array.from(pinnedContainer.querySelectorAll('.haven-tab')).find(t => t.tabEl && t.tabEl.getAttribute('id') === tab.getAttribute('id')); - if (proxy) pinnedContainer.appendChild(proxy); - }); - // Regular - const regularContainer = workspaceEl.querySelector(".haven-workspace-regular-tabs"); - const realRegularTabs = Array.from(gBrowser.tabs).filter(t => !t.pinned && t.getAttribute('zen-workspace-id') === uuid); - realRegularTabs.forEach(tab => { - const proxy = Array.from(regularContainer.querySelectorAll('.haven-tab')).find(t => t.tabEl && t.tabEl.getAttribute('id') === tab.getAttribute('id')); - if (proxy) regularContainer.appendChild(proxy); - }); - } - - function onDragMove(e) { - if (!isDragging || !dragTab) return; - - const sourceWorkspaceEl = innerContainer.querySelector(`.haven-workspace[data-uuid="${dragTab.dataset.workspaceUuid}"]`); - const allWorkspacesList = getAllWorkspaces(); - const sourceIndex = allWorkspacesList.findIndex(ws => ws === sourceWorkspaceEl); - const leftNeighbor = sourceIndex > 0 ? allWorkspacesList[sourceIndex - 1] : null; - const rightNeighbor = sourceIndex < allWorkspacesList.length - 1 ? allWorkspacesList[sourceIndex + 1] : null; - - let newX = dragStartX; // Snap to original X position by default - const newY = e.clientY - dragMouseOffset; // Y axis always follows mouse - - // Check for crossing the 50% threshold to a neighbor workspace - if (rightNeighbor) { - const sourceRect = sourceWorkspaceEl.getBoundingClientRect(); - const rightRect = rightNeighbor.getBoundingClientRect(); - const midpoint = sourceRect.right + (rightRect.left - sourceRect.right) / 2; - if (e.clientX > midpoint) { - newX = e.clientX - dragOffsetX; // Unsnap and follow mouse X - //To-do: animate this, with somthing snappy - } - } - - if (leftNeighbor) { - const sourceRect = sourceWorkspaceEl.getBoundingClientRect(); - const leftRect = leftNeighbor.getBoundingClientRect(); - const midpoint = leftRect.right + (sourceRect.left - leftRect.right) / 2; - if (e.clientX < midpoint) { - newX = e.clientX - dragOffsetX; // Unsnap and follow mouse X + const tabProxy = parseElement(` +
+ + ${faviconUrl ? `` : ''} + + ${tabTitle} +
+ `); + // TODO: Add context menu and copy-link button for grouped tabs + groupTabsContainer.appendChild(tabProxy); } - } - - // Move the tab visually - dragTab.style.top = `${newY}px`; - dragTab.style.left = `${newX}px`; - - const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY); - const hoveredWorkspaceEl = elementUnderCursor ? elementUnderCursor.closest('.haven-workspace') : null; - - // Clean up previous target highlight - const previousTarget = innerContainer.querySelector('.tab-drop-target'); - if (previousTarget && previousTarget !== hoveredWorkspaceEl) { - previousTarget.classList.remove('tab-drop-target'); - } - - if (hoveredWorkspaceEl && hoveredWorkspaceEl.dataset.uuid !== dragTab.dataset.workspaceUuid) { - // We are over a different workspace. Highlight it and hide the placeholder. - hoveredWorkspaceEl.classList.add('tab-drop-target'); - placeholder.style.display = 'none'; - lastContainer = null; // We are no longer in a specific container - - // Hide individual tab movements in the original workspace - sourceWorkspaceEl.querySelectorAll('.haven-tab').forEach(tab => { - if (tab !== dragTab) { - tab.style.transform = ''; + targetContainer.appendChild(groupProxy); + } else if (child.matches('tab:not([zen-essential])')) { + const tabEl = child; + // Proxy: show tab info with icon, title, and copy-link + const tabUrl = tabEl.linkedBrowser?.currentURI?.spec || tabEl.getAttribute('data-url') || tabEl.getAttribute('label') || ''; + const tabTitle = tabEl.getAttribute('label') || tabEl.getAttribute('title') || 'Tab'; + let faviconUrl = tabEl.getAttribute('image') || tabEl.getAttribute('icon') || ''; + if (!faviconUrl && tabUrl.startsWith('http')) { + faviconUrl = `https://www.google.com/s2/favicons?sz=32&domain_url=${encodeURIComponent(tabUrl)}`; } - }); - } else { - // We are over the original workspace (or empty space). Show placeholder and do reordering. - if (hoveredWorkspaceEl) { - hoveredWorkspaceEl.classList.remove('tab-drop-target'); - } - placeholder.style.display = ''; - - // --- NEW INTRA-WORKSPACE LOGIC (Robust) --- - const currentSourceWorkspaceEl = innerContainer.querySelector(`.haven-workspace[data-uuid="${dragTab.dataset.workspaceUuid}"]`); - if (!currentSourceWorkspaceEl) return; - - const sourceContentDiv = currentSourceWorkspaceEl.querySelector('.haven-workspace-content'); - if (!sourceContentDiv) return; - - const sourcePinnedContainer = currentSourceWorkspaceEl.querySelector('.haven-workspace-pinned-tabs'); - const sourceRegularContainer = currentSourceWorkspaceEl.querySelector('.haven-workspace-regular-tabs'); - - let currentTargetContainer = null; - const contentRect = sourceContentDiv.getBoundingClientRect(); - - // Determine which container (pinned/regular) the cursor is over, only if inside content area - if (e.clientX >= contentRect.left && e.clientX <= contentRect.right && e.clientY >= contentRect.top && e.clientY <= contentRect.bottom) { - let pinnedRect = sourcePinnedContainer ? sourcePinnedContainer.getBoundingClientRect() : null; - let regularRect = sourceRegularContainer ? sourceRegularContainer.getBoundingClientRect() : null; - - // Prioritize the container the cursor is physically inside - if (pinnedRect && e.clientY >= pinnedRect.top && e.clientY <= pinnedRect.bottom) { - currentTargetContainer = sourcePinnedContainer; - } else if (regularRect && e.clientY >= regularRect.top && e.clientY <= regularRect.bottom) { - currentTargetContainer = sourceRegularContainer; - } else { - // Fallback for empty space between containers - if (sourcePinnedContainer && sourceRegularContainer) { - // If cursor is above the start of regular container, it's pinned territory - if (e.clientY < regularRect.top) { - currentTargetContainer = sourcePinnedContainer; - } else { - currentTargetContainer = sourceRegularContainer; - } - } else if (sourcePinnedContainer) { - currentTargetContainer = sourcePinnedContainer; - } else if (sourceRegularContainer) { - currentTargetContainer = sourceRegularContainer; - } - } - } - - // If we found a valid container, position the placeholder there - if (currentTargetContainer) { - if (lastContainer !== currentTargetContainer) { - lastContainer = currentTargetContainer; - } - const afterElement = getTabAfterElement(currentTargetContainer, e.clientY); - if (afterElement) { - currentTargetContainer.insertBefore(placeholder, afterElement); - } else { - currentTargetContainer.appendChild(placeholder); + // HTML part (tabProxy) + const tabProxy = parseElement(` +
+ + ${faviconUrl ? `` : ''} + + ${tabTitle} +
+ `); + + // XUL part (copy-link button) + appendXUL( + tabProxy, + ``, + null, + true + ); + tabProxy.addEventListener('contextmenu', (e) => { + e.preventDefault(); + if (tabEl && typeof tabEl.dispatchEvent === 'function') { + const evt = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + view: window, + clientX: e.clientX, + clientY: e.clientY, + screenX: e.screenX, + screenY: e.screenY, + button: 2 + }); + tabEl.dispatchEvent(evt); } - } - - // Animate other tabs in the placeholder's container to make space - getAllWorkspaces().forEach(ws => { - ws.querySelectorAll('.haven-tab').forEach(tab => { - if (tab === dragTab) return; - - // Reset transform if tab is not in the same container as the placeholder - if (!placeholder.parentNode || tab.parentNode !== placeholder.parentNode) { - tab.style.transform = ''; - return; + }); + tabProxy.querySelector('.copy-link').addEventListener('click', (e) => { + e.stopPropagation(); + console.log(`[ZenHaven] Copying URL for tab: ${tabTitle} (${tabUrl})`); + if (tabUrl) { + // Try modern clipboard API first + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(tabUrl).then(() => { + if (typeof gZenUIManager !== 'undefined' && gZenUIManager.showToast) { + gZenUIManager.showToast('zen-copy-current-url-confirmation'); + } + }).catch(() => { + // Fallback if clipboard API fails + console.error(`[ZenHaven] Clipboard API failed, falling back to execCommand for tab: ${tabTitle} (${tabUrl})`); + }); + } else { + console.error(`[ZenHaven] Clipboard API failed, falling back to execCommand for tab: ${tabTitle} (${tabUrl})`); } - - const tabRect = tab.getBoundingClientRect(); - const placeholderRect = placeholder.getBoundingClientRect(); - const isTouching = !(placeholderRect.bottom < tabRect.top + 5 || placeholderRect.top > tabRect.bottom - 5); - - if (isTouching) { - if (!tab.style.transition) { - tab.style.transition = 'transform 0.15s cubic-bezier(.4,1.3,.5,1)'; - } - if (tabRect.top < placeholderRect.top) { // tab is above placeholder - const moveDistance = Math.min(15, Math.abs(tabRect.bottom - placeholderRect.top)); - tab.style.transform = `translateY(-${moveDistance}px)`; - } else if (tabRect.top > placeholderRect.top) { // tab is below placeholder - const moveDistance = Math.min(15, Math.abs(tabRect.top - placeholderRect.bottom)); - tab.style.transform = `translateY(${moveDistance}px)`; + } + }); + // --- Drag-and-drop logic for tabs --- + tabProxy.tabEl = tabEl; // Attach real tab reference + tabProxy.dataset.tabId = tabEl.getAttribute('id') || ''; + tabProxy.dataset.pinned = tabEl.hasAttribute('pinned') ? 'true' : 'false'; + tabProxy.dataset.workspaceUuid = uuid; + // Track original parent and index for restoration + let originalTabParent = null; + let originalTabIndex = null; + tabProxy.addEventListener('dragstart', (e) => { + tabProxy.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', tabProxy.dataset.tabId); + tabProxy.parentNode.classList.add('drag-source'); + contentDiv.classList.add('tab-drag-context'); + // Save original parent and index + originalTabParent = tabProxy.parentNode; + originalTabIndex = Array.from(tabProxy.parentNode.children).indexOf(tabProxy); + }); + tabProxy.addEventListener('dragend', (e) => { + tabProxy.classList.remove('dragging'); + document.querySelectorAll('.haven-workspace-pinned-tabs, .haven-workspace-regular-tabs').forEach(c => c.classList.remove('drag-over', 'drag-source')); + contentDiv.classList.remove('tab-drag-context'); + // If not in a valid container, restore to original position + const validContainers = [pinnedTabsContainer, regularTabsContainer]; + if (!validContainers.includes(tabProxy.parentNode)) { + if (originalTabParent && originalTabIndex !== null) { + const children = Array.from(originalTabParent.children); + if (children.length > originalTabIndex) { + originalTabParent.insertBefore(tabProxy, children[originalTabIndex]); + } else { + originalTabParent.appendChild(tabProxy); } - } else { - tab.style.transform = ''; } + } + originalTabParent = null; + originalTabIndex = null; + }); + // Prevent default drop on document/body to avoid accidental drops outside + if (!window.__zenTabDropPrevented) { + window.addEventListener('dragover', e => e.preventDefault()); + window.addEventListener('drop', e => e.preventDefault()); + window.__zenTabDropPrevented = true; + } + // Only allow drop on containers within this workspace + [pinnedTabsContainer, regularTabsContainer].forEach(container => { + container.addEventListener('dragover', (e) => { + // Only allow drop if this workspace is the drag context + if (!contentDiv.classList.contains('tab-drag-context')) return; + e.preventDefault(); + container.classList.add('drag-over'); }); - }); - } - } - - function onDragEnd(e) { - if (dragHoldTimeout) { - clearTimeout(dragHoldTimeout); - dragHoldTimeout = null; - } - if (!isDragging || !dragTab) return; - - const currentDropTarget = innerContainer.querySelector('.tab-drop-target'); - const dropOnNewWorkspace = currentDropTarget && currentDropTarget.dataset.uuid !== dragTab.dataset.workspaceUuid; - - if (dropOnNewWorkspace) { - // --- Dropped on a new workspace --- - const tabToMove = tabEl; - const targetUuid = currentDropTarget.dataset.uuid; - currentDropTarget.classList.remove('tab-drop-target'); - - const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY); - const targetPinnedContainer = currentDropTarget.querySelector('.haven-workspace-pinned-tabs'); - const targetRegularContainer = currentDropTarget.querySelector('.haven-workspace-regular-tabs'); - - let shouldBePinned = false; - const isOverPinned = !!(elementUnderCursor && elementUnderCursor.closest('.haven-workspace-pinned-tabs')); - const isOverRegular = !!(elementUnderCursor && elementUnderCursor.closest('.haven-workspace-regular-tabs')); - - if (isOverPinned) { - shouldBePinned = true; - } else if (isOverRegular) { - shouldBePinned = false; - } else { - // Fallback for when not dropping directly on a container (e.g., workspace header) - if (targetPinnedContainer && !targetRegularContainer) { - shouldBePinned = true; // Only pinned exists, so must be pinned. - } else if (targetPinnedContainer && targetRegularContainer) { - const regularRect = targetRegularContainer.getBoundingClientRect(); - // If cursor is above the start of the regular container, it's pinned. - shouldBePinned = (e.clientY < regularRect.top); + container.addEventListener('dragleave', (e) => { + container.classList.remove('drag-over'); + }); + container.addEventListener('drop', async (e) => { + // Only allow drop if this workspace is the drag context + if (!contentDiv.classList.contains('tab-drag-context')) return; + e.preventDefault(); + container.classList.remove('drag-over'); + const dragging = container.querySelector('.dragging') || document.querySelector('.haven-tab.dragging'); + if (!dragging) return; + // Only allow drop if the tab belongs to this workspace + if (dragging.dataset.workspaceUuid !== uuid) return; + // Always use vertical position to determine drop location + const after = getTabAfterElement(container, e.clientY); + if (after == null) { + container.appendChild(dragging); } else { - // Only regular exists or neither exist, so not pinned. - shouldBePinned = false; + container.insertBefore(dragging, after); } - } - - if (tabToMove && typeof gZenWorkspaces?.moveTabToWorkspace === 'function') { - try{ - tabEl.setProperty('pinned', shouldBePinned) - tabEl.pinned = shouldBePinned - }catch(e){ console.error(e) } - gZenWorkspaces.moveTabToWorkspace(tabEl, targetUuid); - - // Restore tab's styles before moving the DOM proxy - dragTab.style.position = ''; - dragTab.style.top = ''; - dragTab.style.left = ''; - dragTab.style.width = ''; - dragTab.style.height = ''; - dragTab.style.zIndex = ''; - dragTab.style.pointerEvents = ''; - dragTab.style.transition = ''; - dragTab.classList.remove('dragging-tab'); - dragTab.removeAttribute('drag-tab'); - dragTab.style.transform = ''; - - // Find or create the correct container and append the tab proxy - const contentDiv = currentDropTarget.querySelector('.haven-workspace-content'); - let newContainer; - if (shouldBePinned) { - newContainer = currentDropTarget.querySelector('.haven-workspace-pinned-tabs'); - if (!newContainer) { - newContainer = parseElement(`
`); - contentDiv.insertBefore(newContainer, contentDiv.firstChild); - } + // Update pin state if moved between containers + const isPinnedTarget = container === pinnedTabsContainer; + const tabEl = dragging.tabEl; + if (tabEl) { + if (isPinnedTarget) { + tabEl.setAttribute('pinned', 'true'); + } else { + tabEl.removeAttribute('pinned'); + } + } + // Update order in gZenWorkspaces + if (typeof gZenWorkspaces?.reorderTab === 'function') { + const newIndex = Array.from(container.children).indexOf(dragging); + await gZenWorkspaces.reorderTab(tabEl, newIndex, isPinnedTarget); + } + }); + }); + // Helper for reordering within container + function getTabAfterElement(container, y) { + const draggableTabs = [...container.querySelectorAll('.haven-tab:not(.dragging)')]; + return draggableTabs.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; } else { - newContainer = currentDropTarget.querySelector('.haven-workspace-regular-tabs'); - if (!newContainer) { - newContainer = parseElement(`
`); - contentDiv.appendChild(newContainer); - } + return closest; } - newContainer.appendChild(dragTab); - dragTab.dataset.workspaceUuid = targetUuid; // Update the proxy's workspace ID - - } else { - // Failsafe: if move function fails, just remove the proxy - dragTab.remove(); - } - - // Common cleanup for cross-workspace drop - document.body.style.userSelect = ''; - if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); - getAllWorkspaces().forEach(ws => ws.querySelectorAll('.haven-tab').forEach(tab => { - tab.style.transition = ''; - tab.style.transform = ''; - })); - - } else { - // --- Dropped within the same workspace (original logic) --- - if(currentDropTarget) currentDropTarget.classList.remove('tab-drop-target'); - - // Insert tab at placeholder - placeholder.parentNode.insertBefore(dragTab, placeholder); - // Restore tab's styles - dragTab.style.position = ''; - dragTab.style.top = ''; - dragTab.style.left = ''; - dragTab.style.width = ''; - dragTab.style.height = ''; - dragTab.style.zIndex = ''; - dragTab.style.pointerEvents = ''; - dragTab.style.transition = ''; - dragTab.classList.remove('dragging-tab'); - dragTab.removeAttribute('drag-tab'); - dragTab.style.transform = ''; - // Update pin state if moved between containers (should never happen now) - const isPinnedTarget = placeholder.parentNode === pinnedTabsContainer; - const tabEl = dragTab.tabEl; - // --- Ensure tabEl has a unique id --- - if (tabEl && !tabEl.getAttribute('id')) { - tabEl.setAttribute('id', 'zen-tab-' + Math.random().toString(36).slice(2)); - } - if (tabEl) { - if (isPinnedTarget) { - tabEl.setAttribute('pinned', 'true'); - } else { - tabEl.removeAttribute('pinned'); - } + }, { offset: Number.NEGATIVE_INFINITY }).element; } - // --- Update the underlying tab order in the workspace --- - // Always use the real tab's id for matching - function getTabIdList(container) { - return Array.from(container.querySelectorAll('.haven-tab')).map(t => t.tabEl && t.tabEl.getAttribute('id')).filter(Boolean); + // --- End drag-and-drop logic --- + // --- Custom drag-and-drop logic for vertical tabs --- + function getAllTabProxies() { + return [ + ...pinnedTabsContainer.querySelectorAll('.haven-tab'), + ...regularTabsContainer.querySelectorAll('.haven-tab'), + ]; } - // Only update the order within the section - let order, section; - if (isPinnedTarget) { - order = getTabIdList(pinnedTabsContainer); - section = 'pinned'; - } else { - order = getTabIdList(regularTabsContainer); - section = 'regular'; - } - // Debug log for tab order - console.log('[ZenHaven] New', section, 'tab order:', order); - // --- Update the real Firefox tab order using gBrowser.moveTabTo --- - // Note: Pinned tab order is global, not per workspace! - function reorderFirefoxPinnedTabs(order) { - // Get all real pinned tabs (global, not per workspace) - const allTabs = Array.from(gBrowser.tabs); - let pinnedTabs = allTabs.filter(t => t.pinned); - console.log('[ZenHaven] Real pinned tabs before reorder:', pinnedTabs.map(t => t.getAttribute('id'))); - // For each tab in the new order, move it to the correct index among pinned tabs - for (let i = 0; i < order.length; i++) { - // Always match by the real tab's id - const tab = allTabs.find(t => t.getAttribute('id') === order[i]); - if (tab && !tab.pinned) { - console.log(`[ZenHaven] Pinning tab ${tab.getAttribute('id')}`); - gBrowser.pinTab(tab); - } - // Always move to index i among pinned tabs - if (tab && pinnedTabs[i] !== tab) { - console.log(`[ZenHaven] Moving tab ${tab.getAttribute('id')} to pinned index ${i}`); - gBrowser.moveTabTo(tab, i); - // After move, update pinnedTabs to reflect the new order - pinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned); - console.log('[ZenHaven] Real pinned tabs after move:', pinnedTabs.map(t => t.getAttribute('id'))); + + let isDragging = false; + let dragTab = null; + let dragStartY = 0; + let dragStartX = 0; + let dragOffsetY = 0; + let dragOffsetX = 0; + let dragMouseOffset = 0; + let placeholder = null; + let lastContainer = null; + let dragHoldTimeout = null; + + // --- Drag-and-drop logic for tabProxy --- + tabProxy.addEventListener('mousedown', async (e) => { + if (e.button !== 0) return; // Only left click + if (e.target.closest('.copy-link')) return; // Don't start drag on copy-link button + const isPinned = tabEl && tabEl.hasAttribute('pinned'); + + // Check if workspace is active for pinned tabs + if (isPinned) { + const workspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (!workspaceEl || !workspaceEl.hasAttribute('active')) { + if (window.gZenUIManager && gZenUIManager.showToast) { + gZenUIManager.showToast('Pinned tab order can only be changed in the active workspace.'); + } else { + alert('Pinned tab order can only be changed in the active workspace.'); + } + return; } } - // Final pinned tab order - pinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned); - console.log('[ZenHaven] Final real pinned tab order:', pinnedTabs.map(t => t.getAttribute('id'))); - } - function reorderFirefoxRegularTabs(order) { - const allTabs = Array.from(gBrowser.tabs); - const pinnedCount = gBrowser.tabs.filter(t => t.pinned).length; - for (let i = 0; i < order.length; i++) { - // Always match by the real tab's id - const tab = allTabs.find(t => t.getAttribute('id') === order[i]); - if (tab && tab.pinned) gBrowser.unpinTab(tab); - if (tab) gBrowser.moveTabTo(tab, pinnedCount + i); + + if (tabEl && !tabEl.hasAttribute('id')) { + tabEl.setAttribute('id', 'zen-real-tab-' + Math.random().toString(36).slice(2)); } - } - if (section === 'pinned') { - // Update the workspace's pinned tab order in the data model - if (typeof gZenWorkspaces?.updateWorkspacePinnedOrder === 'function') { - gZenWorkspaces.updateWorkspacePinnedOrder(uuid, order); + e.preventDefault(); + let dragStarted = false; + dragHoldTimeout = setTimeout(() => { + dragStarted = true; + isDragging = true; + dragTab = tabProxy; + const tabRect = tabProxy.getBoundingClientRect(); + dragStartY = tabRect.top; + dragStartX = tabRect.left; + dragMouseOffset = e.clientY - tabRect.top; + dragOffsetY = 0; + dragOffsetX = e.clientX - tabRect.left; + lastContainer = tabProxy.parentNode; + dragTab._dragSection = isPinned ? 'pinned' : 'regular'; + // --- Insert placeholder BEFORE moving tab out of DOM --- + placeholder = document.createElement('div'); + placeholder.className = 'haven-tab drag-placeholder'; + placeholder.style.height = `${tabProxy.offsetHeight}px`; + placeholder.style.width = `${tabProxy.offsetWidth}px`; + tabProxy.parentNode.insertBefore(placeholder, tabProxy); + // --- Now move tab out of flow: fixed position at its original screen position --- + tabProxy.style.position = 'fixed'; + tabProxy.style.top = `${tabRect.top}px`; + tabProxy.style.left = `${tabRect.left}px`; + tabProxy.style.width = `${tabRect.width}px`; + tabProxy.style.height = `${tabRect.height}px`; + tabProxy.style.zIndex = 1000; + tabProxy.style.pointerEvents = 'none'; + tabProxy.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; + tabProxy.style.transform = 'scale(0.92)'; + tabProxy.setAttribute('drag-tab', ''); + tabProxy.classList.add('dragging-tab'); + document.body.appendChild(tabProxy); + document.body.style.userSelect = 'none'; + getAllWorkspaces().forEach(ws => { + ws.querySelectorAll('.haven-tab').forEach(tab => { + if (tab !== dragTab) { + tab.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; + } + }); + }); + window.addEventListener('mousemove', onDragMove); + window.addEventListener('mouseup', onDragEnd); + }, 500); + // If mouse is released before 0.5s, cancel drag + function cancelHold(e2) { + clearTimeout(dragHoldTimeout); + window.removeEventListener('mouseup', cancelHold); + window.removeEventListener('mouseleave', cancelHold); } - // If this workspace is active, also update the real tab strip + window.addEventListener('mouseup', cancelHold); + window.addEventListener('mouseleave', cancelHold); + }); + + // Helper: Sync the custom UI with the real Firefox tab order + function syncCustomUIWithRealTabs() { + // For the active workspace only const workspaceEl = gZenWorkspaces.workspaceElement(uuid); - if (workspaceEl && workspaceEl.hasAttribute('active')) { - reorderFirefoxPinnedTabs(order); + if (!workspaceEl) return; + // Pinned + const pinnedContainer = workspaceEl.querySelector(".haven-workspace-pinned-tabs"); + const realPinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned && t.getAttribute('zen-workspace-id') === uuid); + realPinnedTabs.forEach(tab => { + const proxy = Array.from(pinnedContainer.querySelectorAll('.haven-tab')).find(t => t.tabEl && t.tabEl.getAttribute('id') === tab.getAttribute('id')); + if (proxy) pinnedContainer.appendChild(proxy); + }); + // Regular + const regularContainer = workspaceEl.querySelector(".haven-workspace-regular-tabs"); + const realRegularTabs = Array.from(gBrowser.tabs).filter(t => !t.pinned && t.getAttribute('zen-workspace-id') === uuid); + realRegularTabs.forEach(tab => { + const proxy = Array.from(regularContainer.querySelectorAll('.haven-tab')).find(t => t.tabEl && t.tabEl.getAttribute('id') === tab.getAttribute('id')); + if (proxy) regularContainer.appendChild(proxy); + }); + } + + function onDragMove(e) { + if (!isDragging || !dragTab) return; + + const sourceWorkspaceEl = innerContainer.querySelector(`.haven-workspace[data-uuid="${dragTab.dataset.workspaceUuid}"]`); + const allWorkspacesList = getAllWorkspaces(); + const sourceIndex = allWorkspacesList.findIndex(ws => ws === sourceWorkspaceEl); + const leftNeighbor = sourceIndex > 0 ? allWorkspacesList[sourceIndex - 1] : null; + const rightNeighbor = sourceIndex < allWorkspacesList.length - 1 ? allWorkspacesList[sourceIndex + 1] : null; + + let newX = dragStartX; // Snap to original X position by default + const newY = e.clientY - dragMouseOffset; // Y axis always follows mouse + + // Check for crossing the 50% threshold to a neighbor workspace + if (rightNeighbor) { + const sourceRect = sourceWorkspaceEl.getBoundingClientRect(); + const rightRect = rightNeighbor.getBoundingClientRect(); + const midpoint = sourceRect.right + (rightRect.left - sourceRect.right) / 2; + if (e.clientX > midpoint) { + newX = e.clientX - dragOffsetX; // Unsnap and follow mouse X + //To-do: animate this, with somthing snappy + } + } + + if (leftNeighbor) { + const sourceRect = sourceWorkspaceEl.getBoundingClientRect(); + const leftRect = leftNeighbor.getBoundingClientRect(); + const midpoint = leftRect.right + (sourceRect.left - leftRect.right) / 2; + if (e.clientX < midpoint) { + newX = e.clientX - dragOffsetX; // Unsnap and follow mouse X + } + } + + // Move the tab visually + dragTab.style.top = `${newY}px`; + dragTab.style.left = `${newX}px`; + + const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY); + const hoveredWorkspaceEl = elementUnderCursor ? elementUnderCursor.closest('.haven-workspace') : null; + + // Clean up previous target highlight + const previousTarget = innerContainer.querySelector('.tab-drop-target'); + if (previousTarget && previousTarget !== hoveredWorkspaceEl) { + previousTarget.classList.remove('tab-drop-target'); + } + + if (hoveredWorkspaceEl && hoveredWorkspaceEl.dataset.uuid !== dragTab.dataset.workspaceUuid) { + // We are over a different workspace. Highlight it and hide the placeholder. + hoveredWorkspaceEl.classList.add('tab-drop-target'); + placeholder.style.display = 'none'; + lastContainer = null; // We are no longer in a specific container + + // Hide individual tab movements in the original workspace + sourceWorkspaceEl.querySelectorAll('.haven-tab').forEach(tab => { + if (tab !== dragTab) { + tab.style.transform = ''; + } + }); + } else { + // We are over the original workspace (or empty space). Show placeholder and do reordering. + if (hoveredWorkspaceEl) { + hoveredWorkspaceEl.classList.remove('tab-drop-target'); + } + placeholder.style.display = ''; + + // --- NEW INTRA-WORKSPACE LOGIC (Robust) --- + const currentSourceWorkspaceEl = innerContainer.querySelector(`.haven-workspace[data-uuid="${dragTab.dataset.workspaceUuid}"]`); + if (!currentSourceWorkspaceEl) return; + + const sourceContentDiv = currentSourceWorkspaceEl.querySelector('.haven-workspace-content'); + if (!sourceContentDiv) return; + + const sourcePinnedContainer = currentSourceWorkspaceEl.querySelector('.haven-workspace-pinned-tabs'); + const sourceRegularContainer = currentSourceWorkspaceEl.querySelector('.haven-workspace-regular-tabs'); + + let currentTargetContainer = null; + const contentRect = sourceContentDiv.getBoundingClientRect(); + + // Determine which container (pinned/regular) the cursor is over, only if inside content area + if (e.clientX >= contentRect.left && e.clientX <= contentRect.right && e.clientY >= contentRect.top && e.clientY <= contentRect.bottom) { + let pinnedRect = sourcePinnedContainer ? sourcePinnedContainer.getBoundingClientRect() : null; + let regularRect = sourceRegularContainer ? sourceRegularContainer.getBoundingClientRect() : null; + + // Prioritize the container the cursor is physically inside + if (pinnedRect && e.clientY >= pinnedRect.top && e.clientY <= pinnedRect.bottom) { + currentTargetContainer = sourcePinnedContainer; + } else if (regularRect && e.clientY >= regularRect.top && e.clientY <= regularRect.bottom) { + currentTargetContainer = sourceRegularContainer; + } else { + // Fallback for empty space between containers + if (sourcePinnedContainer && sourceRegularContainer) { + // If cursor is above the start of regular container, it's pinned territory + if (e.clientY < regularRect.top) { + currentTargetContainer = sourcePinnedContainer; + } else { + currentTargetContainer = sourceRegularContainer; + } + } else if (sourcePinnedContainer) { + currentTargetContainer = sourcePinnedContainer; + } else if (sourceRegularContainer) { + currentTargetContainer = sourceRegularContainer; + } + } + } + + // If we found a valid container, position the placeholder there + if (currentTargetContainer) { + if (lastContainer !== currentTargetContainer) { + lastContainer = currentTargetContainer; + } + const afterElement = getTabAfterElement(currentTargetContainer, e.clientY); + if (afterElement) { + currentTargetContainer.insertBefore(placeholder, afterElement); + } else { + currentTargetContainer.appendChild(placeholder); + } + } + + // Animate other tabs in the placeholder's container to make space + getAllWorkspaces().forEach(ws => { + ws.querySelectorAll('.haven-tab').forEach(tab => { + if (tab === dragTab) return; + + // Reset transform if tab is not in the same container as the placeholder + if (!placeholder.parentNode || tab.parentNode !== placeholder.parentNode) { + tab.style.transform = ''; + return; + } + + const tabRect = tab.getBoundingClientRect(); + const placeholderRect = placeholder.getBoundingClientRect(); + const isTouching = !(placeholderRect.bottom < tabRect.top + 5 || placeholderRect.top > tabRect.bottom - 5); + + if (isTouching) { + if (!tab.style.transition) { + tab.style.transition = 'transform 0.15s cubic-bezier(.4,1.3,.5,1)'; + } + if (tabRect.top < placeholderRect.top) { // tab is above placeholder + const moveDistance = Math.min(15, Math.abs(tabRect.bottom - placeholderRect.top)); + tab.style.transform = `translateY(-${moveDistance}px)`; + } else if (tabRect.top > placeholderRect.top) { // tab is below placeholder + const moveDistance = Math.min(15, Math.abs(tabRect.top - placeholderRect.bottom)); + tab.style.transform = `translateY(${moveDistance}px)`; + } + } else { + tab.style.transform = ''; + } + }); + }); } - } else { - reorderFirefoxRegularTabs(order); } - // --- End Firefox tab order update --- - if (typeof gZenWorkspaces?.reorderTabsInWorkspace === 'function') { - if (isPinnedTarget) { - gZenWorkspaces.reorderTabsInWorkspace(uuid, order, getTabIdList(regularTabsContainer)); + + function onDragEnd(e) { + if (dragHoldTimeout) { + clearTimeout(dragHoldTimeout); + dragHoldTimeout = null; + } + if (!isDragging || !dragTab) return; + + const currentDropTarget = innerContainer.querySelector('.tab-drop-target'); + const dropOnNewWorkspace = currentDropTarget && currentDropTarget.dataset.uuid !== dragTab.dataset.workspaceUuid; + + if (dropOnNewWorkspace) { + // --- Dropped on a new workspace --- + const tabToMove = tabEl; + const targetUuid = currentDropTarget.dataset.uuid; + currentDropTarget.classList.remove('tab-drop-target'); + + const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY); + const targetPinnedContainer = currentDropTarget.querySelector('.haven-workspace-pinned-tabs'); + const targetRegularContainer = currentDropTarget.querySelector('.haven-workspace-regular-tabs'); + + let shouldBePinned = false; + const isOverPinned = !!(elementUnderCursor && elementUnderCursor.closest('.haven-workspace-pinned-tabs')); + const isOverRegular = !!(elementUnderCursor && elementUnderCursor.closest('.haven-workspace-regular-tabs')); + + if (isOverPinned) { + shouldBePinned = true; + } else if (isOverRegular) { + shouldBePinned = false; + } else { + // Fallback for when not dropping directly on a container (e.g., workspace header) + if (targetPinnedContainer && !targetRegularContainer) { + shouldBePinned = true; // Only pinned exists, so must be pinned. + } else if (targetPinnedContainer && targetRegularContainer) { + const regularRect = targetRegularContainer.getBoundingClientRect(); + // If cursor is above the start of the regular container, it's pinned. + shouldBePinned = (e.clientY < regularRect.top); + } else { + // Only regular exists or neither exist, so not pinned. + shouldBePinned = false; + } + } + + if (tabToMove && typeof gZenWorkspaces?.moveTabToWorkspace === 'function') { + try{ + tabEl.setProperty('pinned', shouldBePinned) + tabEl.pinned = shouldBePinned + }catch(e){ console.error(e) } + gZenWorkspaces.moveTabToWorkspace(tabEl, targetUuid); + + // Restore tab's styles before moving the DOM proxy + dragTab.style.position = ''; + dragTab.style.top = ''; + dragTab.style.left = ''; + dragTab.style.width = ''; + dragTab.style.height = ''; + dragTab.style.zIndex = ''; + dragTab.style.pointerEvents = ''; + dragTab.style.transition = ''; + dragTab.classList.remove('dragging-tab'); + dragTab.removeAttribute('drag-tab'); + dragTab.style.transform = ''; + + // Find or create the correct container and append the tab proxy + const contentDiv = currentDropTarget.querySelector('.haven-workspace-content'); + let newContainer; + if (shouldBePinned) { + newContainer = currentDropTarget.querySelector('.haven-workspace-pinned-tabs'); + if (!newContainer) { + newContainer = parseElement(`
`); + contentDiv.insertBefore(newContainer, contentDiv.firstChild); + } + } else { + newContainer = currentDropTarget.querySelector('.haven-workspace-regular-tabs'); + if (!newContainer) { + newContainer = parseElement(`
`); + contentDiv.appendChild(newContainer); + } + } + newContainer.appendChild(dragTab); + dragTab.dataset.workspaceUuid = targetUuid; // Update the proxy's workspace ID + + } else { + // Failsafe: if move function fails, just remove the proxy + dragTab.remove(); + } + + // Common cleanup for cross-workspace drop + document.body.style.userSelect = ''; + if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); + getAllWorkspaces().forEach(ws => ws.querySelectorAll('.haven-tab').forEach(tab => { + tab.style.transition = ''; + tab.style.transform = ''; + })); + } else { - gZenWorkspaces.reorderTabsInWorkspace(uuid, getTabIdList(pinnedTabsContainer), order); + // --- Dropped within the same workspace (original logic) --- + if(currentDropTarget) currentDropTarget.classList.remove('tab-drop-target'); + + // Insert tab at placeholder + placeholder.parentNode.insertBefore(dragTab, placeholder); + // Restore tab's styles + dragTab.style.position = ''; + dragTab.style.top = ''; + dragTab.style.left = ''; + dragTab.style.width = ''; + dragTab.style.height = ''; + dragTab.style.zIndex = ''; + dragTab.style.pointerEvents = ''; + dragTab.style.transition = ''; + dragTab.classList.remove('dragging-tab'); + dragTab.removeAttribute('drag-tab'); + dragTab.style.transform = ''; + // Update pin state if moved between containers (should never happen now) + const isPinnedTarget = placeholder.parentNode === pinnedTabsContainer; + const tabEl = dragTab.tabEl; + // --- Ensure tabEl has a unique id --- + if (tabEl && !tabEl.getAttribute('id')) { + tabEl.setAttribute('id', 'zen-tab-' + Math.random().toString(36).slice(2)); + } + if (tabEl) { + if (isPinnedTarget) { + tabEl.setAttribute('pinned', 'true'); + } else { + tabEl.removeAttribute('pinned'); + } + } + // --- Update the underlying tab order in the workspace --- + // Always use the real tab's id for matching + function getTabIdList(container) { + return Array.from(container.querySelectorAll('.haven-tab')).map(t => t.tabEl && t.tabEl.getAttribute('id')).filter(Boolean); + } + // Only update the order within the section + let order, section; + if (isPinnedTarget) { + order = getTabIdList(pinnedTabsContainer); + section = 'pinned'; + } else { + order = getTabIdList(regularTabsContainer); + section = 'regular'; + } + // Debug log for tab order + console.log('[ZenHaven] New', section, 'tab order:', order); + // --- Update the real Firefox tab order using gBrowser.moveTabTo --- + // Note: Pinned tab order is global, not per workspace! + function reorderFirefoxPinnedTabs(order) { + // Get all real pinned tabs (global, not per workspace) + const allTabs = Array.from(gBrowser.tabs); + let pinnedTabs = allTabs.filter(t => t.pinned); + console.log('[ZenHaven] Real pinned tabs before reorder:', pinnedTabs.map(t => t.getAttribute('id'))); + // For each tab in the new order, move it to the correct index among pinned tabs + for (let i = 0; i < order.length; i++) { + // Always match by the real tab's id + const tab = allTabs.find(t => t.getAttribute('id') === order[i]); + if (tab && !tab.pinned) { + console.log(`[ZenHaven] Pinning tab ${tab.getAttribute('id')}`); + gBrowser.pinTab(tab); + } + // Always move to index i among pinned tabs + if (tab && pinnedTabs[i] !== tab) { + console.log(`[ZenHaven] Moving tab ${tab.getAttribute('id')} to pinned index ${i}`); + gBrowser.moveTabTo(tab, i); + // After move, update pinnedTabs to reflect the new order + pinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned); + console.log('[ZenHaven] Real pinned tabs after move:', pinnedTabs.map(t => t.getAttribute('id'))); + } + } + // Final pinned tab order + pinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned); + console.log('[ZenHaven] Final real pinned tab order:', pinnedTabs.map(t => t.getAttribute('id'))); + } + function reorderFirefoxRegularTabs(order) { + const allTabs = Array.from(gBrowser.tabs); + const pinnedCount = gBrowser.tabs.filter(t => t.pinned).length; + for (let i = 0; i < order.length; i++) { + // Always match by the real tab's id + const tab = allTabs.find(t => t.getAttribute('id') === order[i]); + if (tab && tab.pinned) gBrowser.unpinTab(tab); + if (tab) gBrowser.moveTabTo(tab, pinnedCount + i); + } + } + if (section === 'pinned') { + // Update the workspace's pinned tab order in the data model + if (typeof gZenWorkspaces?.updateWorkspacePinnedOrder === 'function') { + gZenWorkspaces.updateWorkspacePinnedOrder(uuid, order); + } + // If this workspace is active, also update the real tab strip + const workspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (workspaceEl && workspaceEl.hasAttribute('active')) { + reorderFirefoxPinnedTabs(order); + } + } else { + reorderFirefoxRegularTabs(order); + } + // --- End Firefox tab order update --- + if (typeof gZenWorkspaces?.reorderTabsInWorkspace === 'function') { + if (isPinnedTarget) { + gZenWorkspaces.reorderTabsInWorkspace(uuid, order, getTabIdList(regularTabsContainer)); + } else { + gZenWorkspaces.reorderTabsInWorkspace(uuid, getTabIdList(pinnedTabsContainer), order); + } + } else if (typeof gZenWorkspaces?.reorderTab === 'function') { + const newIndex = Array.from(placeholder.parentNode.children).indexOf(dragTab); + gZenWorkspaces.reorderTab(tabEl, newIndex, isPinnedTarget); + } + document.body.style.userSelect = ''; + if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); + // After drop, reset all transforms and transitions immediately + getAllTabProxies().forEach(tab => { + tab.style.transition = ''; + tab.style.transform = ''; + }); + // --- Always sync the custom UI with the real tab order after a move --- + setTimeout(syncCustomUIWithRealTabs, 0); } - } else if (typeof gZenWorkspaces?.reorderTab === 'function') { - const newIndex = Array.from(placeholder.parentNode.children).indexOf(dragTab); - gZenWorkspaces.reorderTab(tabEl, newIndex, isPinnedTarget); + isDragging = false; + dragTab = null; + placeholder = null; + window.removeEventListener('mousemove', onDragMove); + window.removeEventListener('mouseup', onDragEnd); } - document.body.style.userSelect = ''; - if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); - // After drop, reset all transforms and transitions immediately - getAllTabProxies().forEach(tab => { - tab.style.transition = ''; - tab.style.transform = ''; - }); - // --- Always sync the custom UI with the real tab order after a move --- - setTimeout(syncCustomUIWithRealTabs, 0); + // --- End custom drag-and-drop logic --- + targetContainer.appendChild(tabProxy); } - isDragging = false; - dragTab = null; - placeholder = null; - window.removeEventListener('mousemove', onDragMove); - window.removeEventListener('mouseup', onDragEnd); - } - // --- End custom drag-and-drop logic --- - if (tabEl.hasAttribute("pinned")) { - pinnedTabsContainer.appendChild(tabProxy); - } else { - regularTabsContainer.appendChild(tabProxy); - } - }); + } + } + + const realWorkspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (realWorkspaceEl) { + const realPinnedContainer = realWorkspaceEl.querySelector('.zen-workspace-pinned-tabs-section'); + if (realPinnedContainer) processChildren(realPinnedContainer.children, pinnedTabsContainer); + + const realRegularContainer = realWorkspaceEl.querySelector('.zen-workspace-normal-tabs-section'); + if (realRegularContainer) processChildren(realRegularContainer.children, regularTabsContainer); + } else { + // Fallback to old logic if real element not found. This will not show tab groups. + allTabs + .filter( + (tabEl) => + tabEl && + tabEl.getAttribute("zen-workspace-id") === uuid && + !tabEl.hasAttribute("zen-essential"), + ) + .forEach((tabEl) => { + processChildren([tabEl], tabEl.hasAttribute("pinned") ? pinnedTabsContainer : regularTabsContainer); + }); + } + if (pinnedTabsContainer.hasChildNodes()) { contentDiv.appendChild(pinnedTabsContainer); } diff --git a/style.css b/style.css index 2535abc..ac7412b 100644 --- a/style.css +++ b/style.css @@ -75,6 +75,25 @@ body[library="true"] { } } +.haven-tab-group .haven-tab-group-header { + cursor: pointer; +} + +.haven-tab-group .haven-tab-group-collapse-icon { + transition: transform 0.2s ease-in-out; + display: inline-flex; + align-items: center; + margin-right: 4px; +} + +.haven-tab-group.collapsed .haven-tab-group-collapse-icon { + transform: rotate(-90deg); +} + +.haven-tab-group.collapsed .haven-tab-group-tabs { + display: none; +} + #zen-library-sidebar { padding-inline: 5px !important; height: 100vh;