Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions desktop-app/resources/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -11678,6 +11678,12 @@ document.addEventListener("DOMContentLoaded", async function () {
if (tabMenu) item.appendChild(tabMenu.button);
item.appendChild(closeButton);

// Suppress middle-button autoscroll, including over nested action buttons.
// Capture runs before their mousedown propagation guards.
item.addEventListener('mousedown', function(event) {
if (event.button === 1) event.preventDefault();
}, { capture: true });

item.addEventListener('contextmenu', function(event) {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -11729,6 +11735,16 @@ document.addEventListener("DOMContentLoaded", async function () {
if (tabId) switchTab(tabId);
};

tabList.onauxclick = function(event) {
if (event.button !== 1) return;
const tabItem = event.target.closest('.tab-item');
if (!tabItem) return;
event.preventDefault();
event.stopPropagation();
const tabId = tabItem.getAttribute('data-tab-id');
if (tabId) closeTab(tabId);
};

tabList.oncontextmenu = function(event) {
if (event.target.closest('.tab-item')) return;
const activeTab = tabs.find(function(tab) { return tab.id === activeTabId && isTabOpen(tab); });
Expand Down
16 changes: 16 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -11678,6 +11678,12 @@ document.addEventListener("DOMContentLoaded", async function () {
if (tabMenu) item.appendChild(tabMenu.button);
item.appendChild(closeButton);

// Suppress middle-button autoscroll, including over nested action buttons.
// Capture runs before their mousedown propagation guards.
item.addEventListener('mousedown', function(event) {
if (event.button === 1) event.preventDefault();
}, { capture: true });

item.addEventListener('contextmenu', function(event) {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -11729,6 +11735,16 @@ document.addEventListener("DOMContentLoaded", async function () {
if (tabId) switchTab(tabId);
};

tabList.onauxclick = function(event) {
if (event.button !== 1) return;
const tabItem = event.target.closest('.tab-item');
if (!tabItem) return;
event.preventDefault();
event.stopPropagation();
const tabId = tabItem.getAttribute('data-tab-id');
if (tabId) closeTab(tabId);
};

tabList.oncontextmenu = function(event) {
if (event.target.closest('.tab-item')) return;
const activeTab = tabs.find(function(tab) { return tab.id === activeTabId && isTabOpen(tab); });
Expand Down
41 changes: 41 additions & 0 deletions tests/e2e/tab-middle-close.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const { test, expect } = require('@playwright/test');
const { openApp } = require('../helpers/app');

test('middle-click closes active and inactive tabs without deleting drafts', async ({ page }) => {
await openApp(page);
const firstId = await page.locator('#tab-list .tab-item.active').getAttribute('data-tab-id');
await page.locator('#tab-new-btn').click();
const secondId = await page.locator('#tab-list .tab-item.active').getAttribute('data-tab-id');
const tab = id => page.locator(`#tab-list .tab-item[data-tab-id="${id}"]`);
await page.locator('#markdown-editor').fill('# preserved draft');
// A genuine middle-button click must not activate the inactive tab first.
await tab(firstId).locator('.tab-title').click({button:'middle'});
await expect(tab(firstId)).toHaveCount(0);
await expect(tab(secondId)).toHaveClass(/active/);
await expect(page.locator('#markdown-editor')).toHaveValue('# preserved draft');
await page.locator('#tab-new-btn').click();
const thirdId = await page.locator('#tab-list .tab-item.active').getAttribute('data-tab-id');
await tab(thirdId).locator('.tab-close-btn').click({button:'middle'});
await expect(tab(thirdId)).toHaveCount(0);
await expect(tab(secondId)).toHaveClass(/active/);
await tab(secondId).locator('.tab-title').click({button:'middle'});
await expect(page.locator('#tab-list .tab-item')).toHaveCount(0);
const row = page.locator(`#document-tree [data-document-id="${secondId}"]`);
await expect(row).toBeVisible();
await row.locator('.document-tree-main').click();
await expect(tab(secondId)).toHaveClass(/active/);
await expect(page.locator('#markdown-editor')).toHaveValue('# preserved draft');
// Only the middle button closes; right-click retains its context menu.
await tab(secondId).locator('.tab-title').click({button:'right'});
await expect(tab(secondId)).toBeVisible();
await expect(page.locator('[data-tab-context-menu="true"]')).toBeVisible();
});

test('middle mousedown is cancelled on both the tab title and nested close button', async ({ page }) => {
await openApp(page);
for (const selector of ['.tab-title', '.tab-close-btn']) {
expect(await page.locator('#tab-list .tab-item').first().locator(selector).evaluate(element =>
element.dispatchEvent(new MouseEvent('mousedown', {button:1,bubbles:true,cancelable:true}))
)).toBe(false);
}
});
Loading