refactor: renderer.js拆分为6个模块(core/tabs/editor/search/sidebar/markdown)
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user