Files
MarkLite/renderer/core.js
T

455 lines
19 KiB
JavaScript

// MarkLite — Core Module: state, DOM refs, settings, utilities, init orchestration
(function () {
'use strict';
// ===== Shared Namespace =====
const ML = {};
window.MarkLite = ML;
// ===== DOM Elements =====
ML.editor = document.getElementById('editor');
ML.preview = document.getElementById('preview');
ML.lineNumbers = document.getElementById('line-numbers');
ML.welcomeScreen = document.getElementById('welcome-screen');
ML.dropOverlay = document.getElementById('drop-overlay');
ML.modifiedBanner = document.getElementById('modified-banner');
ML.statusText = document.getElementById('status-text');
ML.statusCursor = document.getElementById('status-cursor');
ML.statusSize = document.getElementById('status-size');
ML.statusSizeDivider = document.querySelector('.status-size-divider');
ML.resizer = document.getElementById('resizer');
ML.editorPanel = document.getElementById('editor-panel');
ML.previewPanel = document.getElementById('preview-panel');
ML.mainContent = document.getElementById('main-content');
ML.contentWrapper = document.getElementById('content-wrapper');
ML.tabList = document.getElementById('tab-list');
// Sidebar
ML.sidebar = document.getElementById('sidebar');
ML.sidebarTree = document.getElementById('sidebar-tree');
ML.btnOpenFolder = document.getElementById('btn-open-folder');
// Search
ML.searchBar = document.getElementById('search-bar');
ML.searchInput = document.getElementById('search-input');
ML.replaceInput = document.getElementById('replace-input');
ML.searchCount = document.getElementById('search-count');
ML.replaceRow = document.getElementById('replace-row');
ML.btnCase = document.getElementById('btn-case');
ML.btnRegex = document.getElementById('btn-regex');
ML.btnNext = document.getElementById('btn-next');
ML.btnPrev = document.getElementById('btn-prev');
ML.btnSearchClose = document.getElementById('btn-search-close');
ML.btnReplace = document.getElementById('btn-replace');
ML.btnReplaceAll = document.getElementById('btn-replace-all');
// Buttons
ML.btnOpen = document.getElementById('btn-open');
ML.btnSave = document.getElementById('btn-save');
ML.btnSplit = document.getElementById('btn-split');
ML.btnEditor = document.getElementById('btn-editor');
ML.btnPreview = document.getElementById('btn-preview');
ML.btnWelcomeOpen = document.getElementById('btn-welcome-open');
ML.btnWelcomeNew = document.getElementById('btn-welcome-new');
ML.btnDark = document.getElementById('btn-dark');
ML.btnReload = document.getElementById('btn-reload');
ML.btnDismiss = document.getElementById('btn-dismiss');
ML.btnNewTab = document.getElementById('btn-new-tab');
// ===== Shared State =====
ML.viewMode = 'split';
ML.updateTimer = null;
ML.lineCountCache = 0;
ML.currentRootPath = null;
ML.MAX_FILE_SIZE = 20 * 1024 * 1024;
// ===== Settings =====
ML.loadSettings = function () {
try { return JSON.parse(localStorage.getItem('marklite-settings') || '{}'); }
catch { return {}; }
};
ML.saveSetting = function (key, value) {
try {
const s = ML.loadSettings();
s[key] = value;
localStorage.setItem('marklite-settings', JSON.stringify(s));
} catch { /* ignore */ }
};
// ===== Utilities =====
ML.getFileName = function (filePath) {
return filePath.split(/[/\\]/).pop();
};
ML.formatBytes = function (bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
// Character width measurement
var _charWidthCanvas = document.createElement('canvas');
var _charWidthCtx = _charWidthCanvas.getContext('2d');
var _cachedCharWidth = null;
ML.getCharWidth = function () {
if (_cachedCharWidth !== null) return _cachedCharWidth;
var style = getComputedStyle(ML.editor);
_charWidthCtx.font = style.fontSize + ' ' + style.fontFamily;
_cachedCharWidth = _charWidthCtx.measureText('M').width;
return _cachedCharWidth;
};
new MutationObserver(function () { _cachedCharWidth = null; })
.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
// Toast notification
ML.showToast = function (message, duration) {
duration = duration || 3000;
var toast = document.getElementById('toast-notification');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast-notification';
document.getElementById('app').appendChild(toast);
}
toast.textContent = message;
toast.classList.add('show');
clearTimeout(toast._timer);
toast._timer = setTimeout(function () { toast.classList.remove('show'); }, duration);
};
// ===== Dark Mode =====
ML.initDarkMode = function () {
var settings = ML.loadSettings();
var iconDark = document.getElementById('icon-dark');
var iconLight = document.getElementById('icon-light');
function apply(isDark) {
document.documentElement.classList.toggle('dark', isDark);
iconDark.style.display = isDark ? 'none' : '';
iconLight.style.display = isDark ? '' : 'none';
}
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
apply(settings.darkMode !== undefined ? !!settings.darkMode : prefersDark);
ML.btnDark.addEventListener('click', function () {
var isDark = !document.documentElement.classList.contains('dark');
apply(isDark);
ML.saveSetting('darkMode', isDark);
});
};
// ===== File Operations =====
ML.handleOpenFile = async function () {
if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.openFile();
if (result && !result.error) {
ML.createNewTab(result.filePath, result.content);
}
} else {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.md,.markdown,.txt';
input.multiple = true;
input.onchange = function (e) {
Array.from(e.target.files).forEach(function (file) {
var ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
if (!['.md', '.markdown', '.txt'].includes(ext)) return;
if (file.size > ML.MAX_FILE_SIZE) {
alert('"' + file.name + '" 过大(' + ML.formatBytes(file.size) + '),暂不支持超过 20MB 的文件');
return;
}
var reader = new FileReader();
reader.onload = function (ev) { ML.createNewTab(file.name, ev.target.result); };
reader.readAsText(file);
});
};
input.click();
}
};
ML.handleSave = async function () {
var tab = ML.getActiveTab();
if (!tab) return;
if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.saveFile({ filePath: tab.filePath, content: ML.editor.value });
if (result.success) {
tab.filePath = result.filePath;
tab.content = ML.editor.value;
tab.isModified = false;
ML.renderTabBar();
ML.updateTitle();
ML.updateSidebarHighlight();
ML.statusText.textContent = '已保存';
setTimeout(function () { ML.updateTitle(); }, 2000);
}
} else {
var blob = new Blob([ML.editor.value], { type: 'text/markdown' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = tab.filePath || 'untitled.md';
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
tab.isModified = false;
ML.renderTabBar();
ML.updateTitle();
}
};
ML.handleSaveAs = async function () {
var tab = ML.getActiveTab();
if (!tab) return;
if (typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.saveFileAs({ content: ML.editor.value });
if (result.success) {
tab.filePath = result.filePath;
tab.content = ML.editor.value;
tab.isModified = false;
ML.renderTabBar();
ML.updateTitle();
ML.updateSidebarHighlight();
ML.statusText.textContent = '已保存';
setTimeout(function () { ML.updateTitle(); }, 2000);
}
} else {
ML.handleSave();
}
};
// ===== View Mode =====
ML.setViewMode = function (mode) {
ML.viewMode = mode;
var app = document.getElementById('app');
app.classList.remove('mode-editor', 'mode-preview');
ML.btnSplit.classList.remove('active');
ML.btnEditor.classList.remove('active');
ML.btnPreview.classList.remove('active');
switch (mode) {
case 'split': ML.btnSplit.classList.add('active'); break;
case 'editor': ML.btnEditor.classList.add('active'); app.classList.add('mode-editor'); break;
case 'preview': ML.btnPreview.classList.add('active'); app.classList.add('mode-preview'); ML.renderMarkdown(ML.editor.value); break;
}
ML.saveSetting('viewMode', mode);
};
// ===== Resizer =====
ML.initResizer = function () {
var settings = ML.loadSettings();
if (settings.splitRatio) {
ML.editorPanel.style.flex = '0 0 ' + settings.splitRatio + '%';
ML.previewPanel.style.flex = '0 0 ' + (100 - settings.splitRatio) + '%';
}
var isResizing = false;
ML.resizer.addEventListener('mousedown', function (e) {
isResizing = true;
ML.resizer.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!isResizing) return;
var rect = ML.contentWrapper.getBoundingClientRect();
var pct = Math.max(20, Math.min(80, ((e.clientX - rect.left) / rect.width) * 100));
ML.editorPanel.style.flex = '0 0 ' + pct + '%';
ML.previewPanel.style.flex = '0 0 ' + (100 - pct) + '%';
ML.saveSetting('splitRatio', pct);
});
document.addEventListener('mouseup', function () {
if (isResizing) {
isResizing = false;
ML.resizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
};
// ===== Drag & Drop =====
ML.initDragDrop = function () {
var dragCounter = 0;
var allowedExts = ['.md', '.markdown', '.txt'];
document.addEventListener('dragenter', function (e) { e.preventDefault(); e.stopPropagation(); dragCounter++; ML.dropOverlay.classList.remove('hidden'); });
document.addEventListener('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); dragCounter--; if (dragCounter === 0) ML.dropOverlay.classList.add('hidden'); });
document.addEventListener('dragover', function (e) { e.preventDefault(); e.stopPropagation(); });
document.addEventListener('drop', async function (e) {
e.preventDefault(); e.stopPropagation(); dragCounter = 0; ML.dropOverlay.classList.add('hidden');
var files = e.dataTransfer.files;
var rejected = 0;
for (var _i = 0; _i < files.length; _i++) {
var file = files[_i];
var filePath = file.path;
var ext = filePath ? filePath.substring(filePath.lastIndexOf('.')).toLowerCase() : '';
if (!allowedExts.includes(ext)) { rejected++; continue; }
if (file.size > ML.MAX_FILE_SIZE) { ML.showToast('"' + file.name + '" 过大(' + ML.formatBytes(file.size) + '),暂不支持超过 20MB 的文件'); continue; }
if (filePath && typeof window.electronAPI !== 'undefined') {
var result = await window.electronAPI.readFile(filePath);
if (result.success) ML.createNewTab(filePath, result.content);
} else {
var reader = new FileReader();
reader.onload = function (ev) { ML.createNewTab(file.name, ev.target.result); };
reader.readAsText(file);
}
}
if (rejected > 0) ML.showToast('仅支持 .md / .markdown / .txt 文件,已忽略 ' + rejected + ' 个文件');
});
};
// ===== Keyboard Shortcuts =====
ML.initKeyboard = function () {
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 'o') { e.preventDefault(); ML.handleOpenFile(); }
if (e.ctrlKey && e.key === 's' && !e.shiftKey) { e.preventDefault(); ML.handleSave(); }
if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); ML.handleSaveAs(); }
if (e.ctrlKey && e.key === '1') { e.preventDefault(); ML.setViewMode('split'); }
if (e.ctrlKey && e.key === '2') { e.preventDefault(); ML.setViewMode('editor'); }
if (e.ctrlKey && e.key === '3') { e.preventDefault(); ML.setViewMode('preview'); }
if (e.ctrlKey && e.key === 't') { e.preventDefault(); ML.createNewTab(null, ''); }
if (e.ctrlKey && e.key === 'w') { e.preventDefault(); if (ML.activeTabId) ML.closeTab(ML.activeTabId); }
if (e.ctrlKey && e.key === 'Tab') {
e.preventDefault();
if (ML.tabs.length > 1) {
if (e.shiftKey) {
if (ML.mruTabStack.length > 0) {
var targetId = ML.mruTabStack.shift();
if (ML.tabs.find(function (t) { return t.id === targetId; })) ML.switchToTab(targetId);
}
} else {
var idx = ML.getTabIndex(ML.activeTabId);
var next = (idx + 1) % ML.tabs.length;
ML.switchToTab(ML.tabs[next].id);
}
}
}
if (e.ctrlKey && e.key === 'f') { e.preventDefault(); ML.openSearch(false); }
if (e.ctrlKey && e.key === 'h') { e.preventDefault(); ML.openSearch(true); }
if (e.key === 'Escape' && !ML.searchBar.classList.contains('hidden')) { e.preventDefault(); ML.closeSearch(); }
if (e.key === 'Enter' && document.activeElement === ML.searchInput) { e.preventDefault(); e.shiftKey ? ML.findPrev() : ML.findNext(); }
if (e.altKey && e.key === 'c') { e.preventDefault(); ML.toggleSearchOption('case'); }
if (e.altKey && e.key === 'r') { e.preventDefault(); ML.toggleSearchOption('regex'); }
if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); ML.replaceAll(); }
if (e.ctrlKey && e.shiftKey && e.key === 'G') { e.preventDefault(); ML.replaceCurrent(); }
});
};
// ===== Tab Key Support =====
ML.initEditorTab = function () {
ML.editor.addEventListener('keydown', function (e) {
if (e.key === 'Tab') {
e.preventDefault();
var start = ML.editor.selectionStart;
var end = ML.editor.selectionEnd;
if (start !== end) {
var lines = ML.editor.value.substring(start, end).split('\n');
var indented = lines.map(function (line) { return ' ' + line; }).join('\n');
ML.editor.setRangeText(indented, start, end, 'end');
} else {
ML.editor.setRangeText(' ', start, end, 'end');
}
ML.scheduleUpdate();
}
});
};
// ===== External File Modification =====
ML.initFileWatch = function () {
if (typeof window.electronAPI === 'undefined') return;
window.electronAPI.onExternalModification(function (filePath) {
var tab = ML.getActiveTab();
if (tab && tab.filePath === filePath) ML.modifiedBanner.classList.remove('hidden');
});
ML.btnReload.addEventListener('click', async function () {
var tab = ML.getActiveTab();
if (!tab || !tab.filePath) return;
var result = await window.electronAPI.reloadFile();
if (result.success) {
ML.editor.value = result.content;
tab.content = result.content;
tab.isModified = false;
ML.lineCountCache = 0;
ML.updateLineNumbers();
ML.renderMarkdown(result.content);
ML.updateCursorPosition();
ML.updateFileSize();
ML.cachePreviewPositions();
ML.renderTabBar();
ML.updateTitle();
}
ML.modifiedBanner.classList.add('hidden');
});
ML.btnDismiss.addEventListener('click', function () { ML.modifiedBanner.classList.add('hidden'); });
};
// ===== Unsaved Changes Warning =====
ML.initUnsavedWarning = function () {
var hasUnsaved = function () { return ML.tabs.some(function (t) { return t.isModified; }); };
if (typeof window.electronAPI === 'undefined') {
window.addEventListener('beforeunload', function (e) { if (hasUnsaved()) { e.preventDefault(); e.returnValue = ''; } });
return;
}
window.electronAPI.onConfirmClose(function () {
if (!hasUnsaved()) { window.electronAPI.forceClose(); return; }
var shouldClose = confirm('有文件尚未保存,确定要关闭吗?');
if (shouldClose) window.electronAPI.forceClose();
else window.electronAPI.cancelClose();
});
};
// ===== Event Listeners =====
ML.initEventListeners = function () {
ML.editor.addEventListener('input', function () {
var tab = ML.getActiveTab();
if (tab) { tab.isModified = true; tab.content = ML.editor.value; ML.setModified(true); }
ML.scheduleUpdate();
});
ML.editor.addEventListener('scroll', function () {
ML.syncScroll();
ML.lineNumbers.scrollTop = ML.editor.scrollTop;
if (ML.lineCountCache > 2000) ML.renderVirtualLineNumbers(ML.lineCountCache);
});
ML.editor.addEventListener('click', ML.updateCursorPosition);
ML.editor.addEventListener('keyup', ML.updateCursorPosition);
ML.editor.addEventListener('select', ML.updateCursorPosition);
ML.btnOpen.addEventListener('click', ML.handleOpenFile);
ML.btnSave.addEventListener('click', ML.handleSave);
ML.btnSplit.addEventListener('click', function () { ML.setViewMode('split'); });
ML.btnEditor.addEventListener('click', function () { ML.setViewMode('editor'); });
ML.btnPreview.addEventListener('click', function () { ML.setViewMode('preview'); });
ML.btnNewTab.addEventListener('click', function () { ML.createNewTab(null, ''); });
ML.btnWelcomeOpen.addEventListener('click', ML.handleOpenFile);
ML.btnWelcomeNew.addEventListener('click', function () { ML.createNewTab(null, ''); });
if (typeof window.electronAPI !== 'undefined') {
window.electronAPI.removeAllListeners('file:openInTab');
window.electronAPI.removeAllListeners('menu:save');
window.electronAPI.removeAllListeners('menu:saveAs');
window.electronAPI.removeAllListeners('menu:viewMode');
window.electronAPI.onFileOpenInTab(function (data) { ML.createNewTab(data.filePath, data.content); });
window.electronAPI.onMenuSave(function () { ML.handleSave(); });
window.electronAPI.onMenuSaveAs(function () { ML.handleSaveAs(); });
window.electronAPI.onViewModeChange(function (mode) { ML.setViewMode(mode); });
}
};
// ===== Init =====
ML.init = function () {
ML.initMarked();
ML.initDarkMode();
ML.initResizer();
ML.initDragDrop();
ML.initKeyboard();
ML.initEditorTab();
ML.initPreviewLinks();
ML.initEventListeners();
ML.initFileWatch();
ML.initUnsavedWarning();
ML.initSidebar();
ML.initSearchBar();
var settings = ML.loadSettings();
ML.setViewMode(settings.viewMode || 'split');
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ML.init);
} else {
ML.init();
}
})();