784 lines
25 KiB
JavaScript
784 lines
25 KiB
JavaScript
// MarkLite Renderer Process
|
|
(function () {
|
|
'use strict';
|
|
|
|
// ===== DOM Elements =====
|
|
const editor = document.getElementById('editor');
|
|
const preview = document.getElementById('preview');
|
|
const lineNumbers = document.getElementById('line-numbers');
|
|
const welcomeScreen = document.getElementById('welcome-screen');
|
|
const dropOverlay = document.getElementById('drop-overlay');
|
|
const modifiedBanner = document.getElementById('modified-banner');
|
|
const statusText = document.getElementById('status-text');
|
|
const statusCursor = document.getElementById('status-cursor');
|
|
const statusSize = document.getElementById('status-size');
|
|
const statusSizeDivider = document.querySelector('.status-size-divider');
|
|
const resizer = document.getElementById('resizer');
|
|
const editorPanel = document.getElementById('editor-panel');
|
|
const previewPanel = document.getElementById('preview-panel');
|
|
const mainContent = document.getElementById('main-content');
|
|
const tabList = document.getElementById('tab-list');
|
|
|
|
// Buttons
|
|
const btnOpen = document.getElementById('btn-open');
|
|
const btnSave = document.getElementById('btn-save');
|
|
const btnSplit = document.getElementById('btn-split');
|
|
const btnEditor = document.getElementById('btn-editor');
|
|
const btnPreview = document.getElementById('btn-preview');
|
|
const btnWelcomeOpen = document.getElementById('btn-welcome-open');
|
|
const btnWelcomeNew = document.getElementById('btn-welcome-new');
|
|
const btnDark = document.getElementById('btn-dark');
|
|
const btnReload = document.getElementById('btn-reload');
|
|
const btnDismiss = document.getElementById('btn-dismiss');
|
|
const btnNewTab = document.getElementById('btn-new-tab');
|
|
|
|
// ===== Tab State =====
|
|
let tabs = []; // Array of tab objects
|
|
let activeTabId = null;
|
|
let tabIdCounter = 0;
|
|
|
|
// ===== Other State =====
|
|
let viewMode = 'split';
|
|
let updateTimer = null;
|
|
let lineCountCache = 0;
|
|
|
|
// ===== Tab Object =====
|
|
function createTab(filePath, content) {
|
|
const id = ++tabIdCounter;
|
|
return {
|
|
id,
|
|
filePath: filePath || null,
|
|
content: content || '',
|
|
isModified: false,
|
|
scrollTop: 0,
|
|
scrollLeft: 0,
|
|
selectionStart: 0,
|
|
selectionEnd: 0,
|
|
previewScrollTop: 0
|
|
};
|
|
}
|
|
|
|
// ===== localStorage Helpers =====
|
|
function loadSettings() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('marklite-settings') || '{}');
|
|
} catch { return {}; }
|
|
}
|
|
|
|
function saveSetting(key, value) {
|
|
try {
|
|
const s = loadSettings();
|
|
s[key] = value;
|
|
localStorage.setItem('marklite-settings', JSON.stringify(s));
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
// ===== Dark Mode =====
|
|
function initDarkMode() {
|
|
const settings = loadSettings();
|
|
const iconDark = document.getElementById('icon-dark');
|
|
const iconLight = document.getElementById('icon-light');
|
|
|
|
function apply(isDark) {
|
|
document.documentElement.classList.toggle('dark', isDark);
|
|
iconDark.style.display = isDark ? 'none' : '';
|
|
iconLight.style.display = isDark ? '' : 'none';
|
|
}
|
|
|
|
apply(!!settings.darkMode);
|
|
|
|
btnDark.addEventListener('click', () => {
|
|
const isDark = !document.documentElement.classList.contains('dark');
|
|
apply(isDark);
|
|
saveSetting('darkMode', isDark);
|
|
});
|
|
}
|
|
|
|
// ===== Initialize marked.js =====
|
|
function initMarked() {
|
|
if (typeof marked !== 'undefined') {
|
|
const renderer = new marked.Renderer();
|
|
renderer.code = function ({ text, lang }) {
|
|
let highlighted = text;
|
|
if (typeof hljs !== 'undefined') {
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
try { highlighted = hljs.highlight(text, { language: lang }).value; } catch (e) { /* fallback */ }
|
|
} else {
|
|
try { highlighted = hljs.highlightAuto(text).value; } catch (e) { /* fallback */ }
|
|
}
|
|
}
|
|
const langClass = lang ? ` language-${lang}` : '';
|
|
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`;
|
|
};
|
|
marked.setOptions({ gfm: true, breaks: true, pedantic: false });
|
|
marked.use({ renderer });
|
|
}
|
|
}
|
|
|
|
// ===== Markdown Rendering =====
|
|
function renderMarkdown(text) {
|
|
if (typeof marked === 'undefined') {
|
|
preview.innerHTML = '<p style="color: red;">错误: Markdown 解析库未加载</p>';
|
|
return;
|
|
}
|
|
try {
|
|
preview.innerHTML = marked.parse(text);
|
|
} catch (e) {
|
|
preview.innerHTML = '<p style="color: red;">渲染错误: ' + e.message + '</p>';
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
// Only allow http/https protocols
|
|
try {
|
|
const url = new URL(href);
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
|
|
} catch {
|
|
// Relative URLs fail new URL(), but we already handled anchors above
|
|
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;
|
|
if (count === lineCountCache) return;
|
|
lineCountCache = count;
|
|
const fragment = [];
|
|
for (let i = 1; i <= count; i++) {
|
|
fragment.push('<div class="line-num">', i, '</div>');
|
|
}
|
|
lineNumbers.innerHTML = fragment.join('');
|
|
}
|
|
|
|
// ===== Cursor Position =====
|
|
function updateCursorPosition() {
|
|
const text = editor.value;
|
|
const pos = editor.selectionStart;
|
|
const textBefore = text.substring(0, pos);
|
|
const lines = textBefore.split('\n');
|
|
statusCursor.textContent = `行 ${lines.length}, 列 ${lines[lines.length - 1].length + 1}`;
|
|
}
|
|
|
|
// ===== File Size =====
|
|
function formatBytes(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function updateFileSize() {
|
|
const bytes = new Blob([editor.value]).size;
|
|
statusSize.textContent = formatBytes(bytes);
|
|
statusSizeDivider.classList.add('visible');
|
|
}
|
|
|
|
// ===== Scroll Sync =====
|
|
let previewPositions = null;
|
|
|
|
function cachePreviewPositions() {
|
|
previewPositions = [];
|
|
for (let i = 0; i < preview.children.length; i++) {
|
|
previewPositions.push(preview.children[i].offsetTop);
|
|
}
|
|
}
|
|
|
|
function syncScroll() {
|
|
if (viewMode !== 'split') return;
|
|
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 20.8;
|
|
const scrollTop = editor.scrollTop;
|
|
const topLine = Math.floor(scrollTop / lineHeight);
|
|
const totalLines = editor.value.split('\n').length;
|
|
|
|
if (!previewPositions || previewPositions.length === 0) cachePreviewPositions();
|
|
if (!previewPositions || previewPositions.length === 0) return;
|
|
|
|
const ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0;
|
|
const targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1);
|
|
if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) {
|
|
previewPanel.scrollTop = previewPositions[targetIndex];
|
|
}
|
|
}
|
|
|
|
// ===== Schedule Update =====
|
|
function scheduleUpdate() {
|
|
if (updateTimer) clearTimeout(updateTimer);
|
|
updateTimer = setTimeout(() => {
|
|
renderMarkdown(editor.value);
|
|
updateLineNumbers();
|
|
updateFileSize();
|
|
cachePreviewPositions();
|
|
}, 150);
|
|
}
|
|
|
|
// ===== Helper =====
|
|
function getFileName(filePath) {
|
|
return filePath.split(/[/\\]/).pop();
|
|
}
|
|
|
|
// ===== Tab UI =====
|
|
function renderTabBar() {
|
|
tabList.innerHTML = '';
|
|
for (const tab of tabs) {
|
|
const el = document.createElement('div');
|
|
el.className = 'tab-item' + (tab.id === activeTabId ? ' active' : '') + (tab.isModified ? ' modified' : '');
|
|
el.dataset.tabId = tab.id;
|
|
|
|
const name = document.createElement('span');
|
|
name.className = 'tab-name';
|
|
name.textContent = tab.filePath ? getFileName(tab.filePath) : '未命名';
|
|
el.appendChild(name);
|
|
|
|
const close = document.createElement('button');
|
|
close.className = 'tab-close';
|
|
close.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
|
|
close.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
closeTab(tab.id);
|
|
});
|
|
el.appendChild(close);
|
|
|
|
el.addEventListener('click', () => switchToTab(tab.id));
|
|
tabList.appendChild(el);
|
|
}
|
|
}
|
|
|
|
function updateTitle() {
|
|
const tab = getActiveTab();
|
|
if (tab) {
|
|
const name = tab.filePath ? getFileName(tab.filePath) : '未命名';
|
|
const mod = tab.isModified ? ' *' : '';
|
|
document.title = `MarkLite - ${name}${mod}`;
|
|
statusText.textContent = name;
|
|
} else {
|
|
document.title = 'MarkLite';
|
|
statusText.textContent = '就绪';
|
|
}
|
|
}
|
|
|
|
// ===== Tab Operations =====
|
|
function getActiveTab() {
|
|
return tabs.find(t => t.id === activeTabId) || null;
|
|
}
|
|
|
|
function getTabIndex(tabId) {
|
|
return tabs.findIndex(t => t.id === tabId);
|
|
}
|
|
|
|
// Save current editor state into the active tab
|
|
function saveCurrentTabState() {
|
|
const tab = getActiveTab();
|
|
if (!tab) return;
|
|
tab.content = editor.value;
|
|
tab.scrollTop = editor.scrollTop;
|
|
tab.scrollLeft = editor.scrollLeft;
|
|
tab.selectionStart = editor.selectionStart;
|
|
tab.selectionEnd = editor.selectionEnd;
|
|
tab.previewScrollTop = previewPanel.scrollTop;
|
|
}
|
|
|
|
// Load a tab's state into the editor
|
|
function loadTabState(tab) {
|
|
editor.value = tab.content;
|
|
lineCountCache = 0;
|
|
updateLineNumbers();
|
|
renderMarkdown(tab.content);
|
|
updateFileSize();
|
|
cachePreviewPositions();
|
|
|
|
// Restore scroll/selection on next frame
|
|
requestAnimationFrame(() => {
|
|
editor.scrollTop = tab.scrollTop;
|
|
editor.scrollLeft = tab.scrollLeft;
|
|
editor.setSelectionRange(tab.selectionStart, tab.selectionEnd);
|
|
previewPanel.scrollTop = tab.previewScrollTop;
|
|
updateCursorPosition();
|
|
});
|
|
|
|
// Update modified state
|
|
setModified(tab.isModified);
|
|
|
|
// Notify main process about active file
|
|
if (typeof window.electronAPI !== 'undefined') {
|
|
window.electronAPI.tabSwitched(tab.filePath);
|
|
}
|
|
}
|
|
|
|
function setModified(modified) {
|
|
const tab = getActiveTab();
|
|
if (tab) {
|
|
tab.isModified = modified;
|
|
// Toggle class directly instead of rebuilding entire tab bar
|
|
const tabEl = tabList.querySelector(`.tab-item[data-tab-id="${tab.id}"]`);
|
|
if (tabEl) tabEl.classList.toggle('modified', modified);
|
|
}
|
|
updateTitle();
|
|
}
|
|
|
|
// Create a new tab and switch to it
|
|
function createNewTab(filePath, content) {
|
|
// Check if file is already open
|
|
if (filePath) {
|
|
const existing = tabs.find(t => t.filePath === filePath);
|
|
if (existing) {
|
|
switchToTab(existing.id);
|
|
return existing;
|
|
}
|
|
}
|
|
|
|
const tab = createTab(filePath, content);
|
|
tabs.push(tab);
|
|
switchToTab(tab.id);
|
|
return tab;
|
|
}
|
|
|
|
// Switch to a specific tab
|
|
function switchToTab(tabId) {
|
|
if (tabId === activeTabId) return;
|
|
|
|
// Save current state
|
|
saveCurrentTabState();
|
|
|
|
// Switch
|
|
activeTabId = tabId;
|
|
const tab = getActiveTab();
|
|
if (!tab) return;
|
|
|
|
loadTabState(tab);
|
|
renderTabBar();
|
|
updateTitle();
|
|
welcomeScreen.classList.add('hidden');
|
|
}
|
|
|
|
// Close a tab
|
|
function closeTab(tabId) {
|
|
const index = getTabIndex(tabId);
|
|
if (index === -1) return;
|
|
|
|
const tab = tabs[index];
|
|
|
|
// If modified, confirm
|
|
if (tab.isModified) {
|
|
const name = tab.filePath ? getFileName(tab.filePath) : '未命名';
|
|
if (!confirm(`"${name}" 尚未保存,确定要关闭吗?`)) return;
|
|
}
|
|
|
|
// Remove tab
|
|
tabs.splice(index, 1);
|
|
|
|
// If we closed the active tab, switch to another
|
|
if (tabId === activeTabId) {
|
|
if (tabs.length === 0) {
|
|
// No tabs left - show welcome screen
|
|
activeTabId = null;
|
|
editor.value = '';
|
|
preview.innerHTML = '';
|
|
lineNumbers.innerHTML = '';
|
|
lineCountCache = 0;
|
|
statusSize.textContent = '';
|
|
statusSizeDivider.classList.remove('visible');
|
|
welcomeScreen.classList.remove('hidden');
|
|
document.title = 'MarkLite';
|
|
statusText.textContent = '就绪';
|
|
if (typeof window.electronAPI !== 'undefined') {
|
|
window.electronAPI.tabSwitched(null);
|
|
}
|
|
} else {
|
|
// Switch to nearest tab
|
|
const newIndex = Math.min(index, tabs.length - 1);
|
|
activeTabId = tabs[newIndex].id;
|
|
loadTabState(tabs[newIndex]);
|
|
}
|
|
}
|
|
|
|
renderTabBar();
|
|
updateTitle();
|
|
}
|
|
|
|
// ===== File Operations =====
|
|
async function handleOpenFile() {
|
|
if (typeof window.electronAPI !== 'undefined') {
|
|
const result = await window.electronAPI.openFile();
|
|
if (result && !result.error) {
|
|
createNewTab(result.filePath, result.content);
|
|
}
|
|
} else {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.md,.markdown,.txt';
|
|
input.multiple = true;
|
|
input.onchange = (e) => {
|
|
Array.from(e.target.files).forEach(file => {
|
|
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
if (!['.md', '.markdown', '.txt'].includes(ext)) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
createNewTab(file.name, ev.target.result);
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
};
|
|
input.click();
|
|
}
|
|
}
|
|
|
|
async function handleSave() {
|
|
const tab = getActiveTab();
|
|
if (!tab) return;
|
|
|
|
if (typeof window.electronAPI !== 'undefined') {
|
|
const result = await window.electronAPI.saveFile({
|
|
filePath: tab.filePath,
|
|
content: editor.value
|
|
});
|
|
if (result.success) {
|
|
tab.filePath = result.filePath;
|
|
tab.content = editor.value;
|
|
tab.isModified = false;
|
|
renderTabBar();
|
|
updateTitle();
|
|
statusText.textContent = '已保存';
|
|
setTimeout(() => updateTitle(), 2000);
|
|
}
|
|
} else {
|
|
const blob = new Blob([editor.value], { type: 'text/markdown' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = tab.filePath || 'untitled.md';
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
tab.isModified = false;
|
|
renderTabBar();
|
|
updateTitle();
|
|
}
|
|
}
|
|
|
|
async function handleSaveAs() {
|
|
const tab = getActiveTab();
|
|
if (!tab) return;
|
|
|
|
if (typeof window.electronAPI !== 'undefined') {
|
|
const result = await window.electronAPI.saveFileAs({ content: editor.value });
|
|
if (result.success) {
|
|
tab.filePath = result.filePath;
|
|
tab.content = editor.value;
|
|
tab.isModified = false;
|
|
renderTabBar();
|
|
updateTitle();
|
|
statusText.textContent = '已保存';
|
|
setTimeout(() => updateTitle(), 2000);
|
|
}
|
|
} else {
|
|
handleSave();
|
|
}
|
|
}
|
|
|
|
// ===== View Mode =====
|
|
function setViewMode(mode) {
|
|
viewMode = mode;
|
|
const app = document.getElementById('app');
|
|
app.classList.remove('mode-editor', 'mode-preview');
|
|
btnSplit.classList.remove('active');
|
|
btnEditor.classList.remove('active');
|
|
btnPreview.classList.remove('active');
|
|
|
|
switch (mode) {
|
|
case 'split':
|
|
btnSplit.classList.add('active');
|
|
break;
|
|
case 'editor':
|
|
btnEditor.classList.add('active');
|
|
app.classList.add('mode-editor');
|
|
break;
|
|
case 'preview':
|
|
btnPreview.classList.add('active');
|
|
app.classList.add('mode-preview');
|
|
renderMarkdown(editor.value);
|
|
break;
|
|
}
|
|
saveSetting('viewMode', mode);
|
|
}
|
|
|
|
// ===== Resizer =====
|
|
let isResizing = false;
|
|
|
|
function initResizer() {
|
|
const settings = loadSettings();
|
|
if (settings.splitRatio) {
|
|
editorPanel.style.flex = `0 0 ${settings.splitRatio}%`;
|
|
previewPanel.style.flex = `0 0 ${100 - settings.splitRatio}%`;
|
|
}
|
|
|
|
resizer.addEventListener('mousedown', (e) => {
|
|
isResizing = true;
|
|
resizer.classList.add('active');
|
|
document.body.style.cursor = 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
const rect = mainContent.getBoundingClientRect();
|
|
const pct = Math.max(20, Math.min(80, ((e.clientX - rect.left) / rect.width) * 100));
|
|
editorPanel.style.flex = `0 0 ${pct}%`;
|
|
previewPanel.style.flex = `0 0 ${100 - pct}%`;
|
|
saveSetting('splitRatio', pct);
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
resizer.classList.remove('active');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== Drag & Drop =====
|
|
function initDragDrop() {
|
|
let dragCounter = 0;
|
|
|
|
document.addEventListener('dragenter', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dragCounter++;
|
|
dropOverlay.classList.remove('hidden');
|
|
});
|
|
|
|
document.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dragCounter--;
|
|
if (dragCounter === 0) dropOverlay.classList.add('hidden');
|
|
});
|
|
|
|
document.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
|
|
document.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dragCounter = 0;
|
|
dropOverlay.classList.add('hidden');
|
|
|
|
const files = e.dataTransfer.files;
|
|
const allowedExts = ['.md', '.markdown', '.txt'];
|
|
for (const file of files) {
|
|
const filePath = file.path;
|
|
const ext = filePath ? filePath.substring(filePath.lastIndexOf('.')).toLowerCase() : '';
|
|
if (!allowedExts.includes(ext)) continue;
|
|
if (filePath && typeof window.electronAPI !== 'undefined') {
|
|
const result = await window.electronAPI.readFile(filePath);
|
|
if (result.success) {
|
|
createNewTab(filePath, result.content);
|
|
}
|
|
} else {
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => createNewTab(file.name, ev.target.result);
|
|
reader.readAsText(file);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== Keyboard Shortcuts =====
|
|
function initKeyboard() {
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey && e.key === 'o') { e.preventDefault(); handleOpenFile(); }
|
|
if (e.ctrlKey && e.key === 's' && !e.shiftKey) { e.preventDefault(); handleSave(); }
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); handleSaveAs(); }
|
|
if (e.ctrlKey && e.key === '1') { e.preventDefault(); setViewMode('split'); }
|
|
if (e.ctrlKey && e.key === '2') { e.preventDefault(); setViewMode('editor'); }
|
|
if (e.ctrlKey && e.key === '3') { e.preventDefault(); setViewMode('preview'); }
|
|
if (e.ctrlKey && e.key === 't') { e.preventDefault(); createNewTab(null, ''); }
|
|
if (e.ctrlKey && e.key === 'w') { e.preventDefault(); if (activeTabId) closeTab(activeTabId); }
|
|
// Ctrl+Tab / Ctrl+Shift+Tab to cycle tabs
|
|
if (e.ctrlKey && e.key === 'Tab') {
|
|
e.preventDefault();
|
|
if (tabs.length > 1) {
|
|
const idx = getTabIndex(activeTabId);
|
|
const next = e.shiftKey
|
|
? (idx - 1 + tabs.length) % tabs.length
|
|
: (idx + 1) % tabs.length;
|
|
switchToTab(tabs[next].id);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== Tab key support =====
|
|
function initEditorTab() {
|
|
editor.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
if (!document.execCommand('insertText', false, ' ')) {
|
|
const start = editor.selectionStart;
|
|
const end = editor.selectionEnd;
|
|
editor.setRangeText(' ', start, end, 'end');
|
|
}
|
|
scheduleUpdate();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== External File Modification =====
|
|
function initFileWatch() {
|
|
if (typeof window.electronAPI === 'undefined') return;
|
|
|
|
window.electronAPI.onExternalModification((filePath) => {
|
|
// Only show banner if the modified file is the active tab
|
|
const tab = getActiveTab();
|
|
if (tab && tab.filePath === filePath) {
|
|
modifiedBanner.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
btnReload.addEventListener('click', async () => {
|
|
const tab = getActiveTab();
|
|
if (!tab || !tab.filePath) return;
|
|
const result = await window.electronAPI.reloadFile();
|
|
if (result.success) {
|
|
editor.value = result.content;
|
|
tab.content = result.content;
|
|
tab.isModified = false;
|
|
lineCountCache = 0;
|
|
updateLineNumbers();
|
|
renderMarkdown(result.content);
|
|
updateCursorPosition();
|
|
updateFileSize();
|
|
cachePreviewPositions();
|
|
renderTabBar();
|
|
updateTitle();
|
|
}
|
|
modifiedBanner.classList.add('hidden');
|
|
});
|
|
|
|
btnDismiss.addEventListener('click', () => {
|
|
modifiedBanner.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
// ===== Unsaved Changes Warning =====
|
|
function initUnsavedWarning() {
|
|
const hasUnsaved = () => tabs.some(t => t.isModified);
|
|
|
|
if (typeof window.electronAPI === 'undefined') {
|
|
window.addEventListener('beforeunload', (e) => {
|
|
if (hasUnsaved()) { e.preventDefault(); e.returnValue = ''; }
|
|
});
|
|
return;
|
|
}
|
|
|
|
window.electronAPI.onConfirmClose(() => {
|
|
if (!hasUnsaved()) {
|
|
window.electronAPI.forceClose();
|
|
return;
|
|
}
|
|
const shouldClose = confirm('有文件尚未保存,确定要关闭吗?');
|
|
if (shouldClose) {
|
|
window.electronAPI.forceClose();
|
|
} else {
|
|
window.electronAPI.cancelClose();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== Event Listeners =====
|
|
function initEventListeners() {
|
|
editor.addEventListener('input', () => {
|
|
const tab = getActiveTab();
|
|
if (tab) {
|
|
tab.isModified = true;
|
|
tab.content = editor.value;
|
|
setModified(true);
|
|
}
|
|
scheduleUpdate();
|
|
});
|
|
|
|
editor.addEventListener('scroll', syncScroll);
|
|
editor.addEventListener('click', updateCursorPosition);
|
|
editor.addEventListener('keyup', updateCursorPosition);
|
|
editor.addEventListener('select', updateCursorPosition);
|
|
|
|
btnOpen.addEventListener('click', handleOpenFile);
|
|
btnSave.addEventListener('click', handleSave);
|
|
btnSplit.addEventListener('click', () => setViewMode('split'));
|
|
btnEditor.addEventListener('click', () => setViewMode('editor'));
|
|
btnPreview.addEventListener('click', () => setViewMode('preview'));
|
|
btnNewTab.addEventListener('click', () => createNewTab(null, ''));
|
|
|
|
btnWelcomeOpen.addEventListener('click', handleOpenFile);
|
|
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);
|
|
});
|
|
|
|
// Legacy: file opened via old dialog
|
|
window.electronAPI.onFileOpened((data) => {
|
|
createNewTab(data.filePath, data.content);
|
|
});
|
|
|
|
window.electronAPI.onMenuSave(() => handleSave());
|
|
window.electronAPI.onMenuSaveAs(() => handleSaveAs());
|
|
window.electronAPI.onViewModeChange((mode) => setViewMode(mode));
|
|
}
|
|
}
|
|
|
|
// ===== Initialize =====
|
|
function init() {
|
|
initMarked();
|
|
initDarkMode();
|
|
initResizer();
|
|
initDragDrop();
|
|
initKeyboard();
|
|
initEditorTab();
|
|
initPreviewLinks();
|
|
initEventListeners();
|
|
initFileWatch();
|
|
initUnsavedWarning();
|
|
|
|
const settings = loadSettings();
|
|
setViewMode(settings.viewMode || 'split');
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|