// 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(); } })();