fix: 预览选择复制、关闭超时、链接跳转、监听器泄漏

1. 预览区无法选择复制 — #preview 添加 user-select: text
2. IPC 监听器重复注册 — 注册前先 removeAllListeners 防止堆叠
3. 关闭确认取消后 5s 超时仍强制关闭 — 新增 cancelClose IPC 清除超时
4. 预览区链接点击离开应用 — 拦截点击,通过 shell.openExternal 打开
5. 预览区链接光标样式 — .markdown-body a 添加 cursor: pointer
This commit is contained in:
thzxx
2026-05-18 13:55:02 +08:00
parent 394c88e52f
commit aac6a0b073
4 changed files with 56 additions and 3 deletions
+14 -1
View File
@@ -6,6 +6,7 @@ let mainWindow = null;
let activeFilePath = null; // Currently active tab's file path
let pendingFilePath = null;
let fileWatcher = null;
let closeTimeout = null;
function createWindow() {
mainWindow = new BrowserWindow({
@@ -48,7 +49,8 @@ function createWindow() {
mainWindow.close();
return;
}
setTimeout(() => {
// 5s safety timeout in case renderer is unresponsive
closeTimeout = setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.removeAllListeners('close');
mainWindow.close();
@@ -237,11 +239,22 @@ ipcMain.handle('tab:switched', (event, filePath) => {
});
ipcMain.handle('window:forceClose', () => {
if (closeTimeout) {
clearTimeout(closeTimeout);
closeTimeout = null;
}
stopWatching();
mainWindow.removeAllListeners('close');
mainWindow.close();
});
ipcMain.handle('window:cancelClose', () => {
if (closeTimeout) {
clearTimeout(closeTimeout);
closeTimeout = null;
}
});
// Command line file
function getCommandLineFile() {
const args = process.argv.slice(1);
+5 -1
View File
@@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron');
const { contextBridge, ipcRenderer, shell } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// File operations
@@ -15,6 +15,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Window control
forceClose: () => ipcRenderer.invoke('window:forceClose'),
cancelClose: () => ipcRenderer.invoke('window:cancelClose'),
// Shell
openExternal: (url) => shell.openExternal(url),
// Events from main process
onFileOpenInTab: (callback) => ipcRenderer.on('file:openInTab', (event, data) => callback(data)),
+36 -1
View File
@@ -128,6 +128,29 @@
}
}
// Intercept link clicks in preview — open in system browser
function initPreviewLinks() {
preview.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (!link) return;
e.preventDefault();
const href = link.getAttribute('href');
if (!href) return;
// Anchor links: scroll within preview
if (href.startsWith('#')) {
const target = preview.querySelector(href);
if (target) target.scrollIntoView({ behavior: 'smooth' });
return;
}
// External links: open in system browser
if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openExternal) {
window.electronAPI.openExternal(href);
} else {
window.open(href, '_blank', 'noopener');
}
});
}
// ===== Line Numbers =====
function updateLineNumbers() {
const count = editor.value.split('\n').length;
@@ -663,7 +686,11 @@
return;
}
const shouldClose = confirm('有文件尚未保存,确定要关闭吗?');
if (shouldClose) window.electronAPI.forceClose();
if (shouldClose) {
window.electronAPI.forceClose();
} else {
window.electronAPI.cancelClose();
}
});
}
@@ -695,6 +722,13 @@
btnWelcomeNew.addEventListener('click', () => createNewTab(null, ''));
if (typeof window.electronAPI !== 'undefined') {
// Remove old listeners first to prevent stacking
window.electronAPI.removeAllListeners('file:openInTab');
window.electronAPI.removeAllListeners('file:opened');
window.electronAPI.removeAllListeners('menu:save');
window.electronAPI.removeAllListeners('menu:saveAs');
window.electronAPI.removeAllListeners('menu:viewMode');
// Main process sends file to open in a new tab
window.electronAPI.onFileOpenInTab((data) => {
createNewTab(data.filePath, data.content);
@@ -719,6 +753,7 @@
initDragDrop();
initKeyboard();
initEditorTab();
initPreviewLinks();
initEventListeners();
initFileWatch();
initUnsavedWarning();
+1
View File
@@ -431,6 +431,7 @@ html, body {
.markdown-body a {
color: var(--primary);
text-decoration: none;
cursor: pointer;
}
.markdown-body a:hover {