Revert "refactor: renderer.js拆分为6个模块(core/tabs/editor/search/sidebar/markdown)"
This reverts commit e071a26db6.
This commit is contained in:
@@ -1,454 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// MarkLite — Editor Module: line numbers, cursor, file size, scroll sync
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
var ML = window.MarkLite;
|
|
||||||
|
|
||||||
// ===== Line Numbers =====
|
|
||||||
function countNewlines(text) {
|
|
||||||
var count = 1;
|
|
||||||
for (var i = 0, len = text.length; i < len; i++) {
|
|
||||||
if (text.charCodeAt(i) === 10) count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
ML.updateLineNumbers = function () {
|
|
||||||
var count = countNewlines(ML.editor.value);
|
|
||||||
if (count === ML.lineCountCache) return;
|
|
||||||
ML.lineCountCache = count;
|
|
||||||
if (count > 2000) {
|
|
||||||
ML.renderVirtualLineNumbers(count);
|
|
||||||
} else {
|
|
||||||
var arr = new Array(count);
|
|
||||||
for (var i = 0; i < count; i++) arr[i] = '<div class="line-num">' + (i + 1) + '</div>';
|
|
||||||
ML.lineNumbers.innerHTML = arr.join('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.renderVirtualLineNumbers = function (totalLines) {
|
|
||||||
var lineHeight = 20.8;
|
|
||||||
var scrollTop = ML.editor.scrollTop;
|
|
||||||
var viewportHeight = ML.editor.clientHeight;
|
|
||||||
var buffer = 20;
|
|
||||||
var startLine = Math.max(0, Math.floor(scrollTop / lineHeight) - buffer);
|
|
||||||
var endLine = Math.min(totalLines, Math.ceil((scrollTop + viewportHeight) / lineHeight) + buffer);
|
|
||||||
var visibleCount = endLine - startLine;
|
|
||||||
var topPadding = startLine * lineHeight;
|
|
||||||
var bottomPadding = (totalLines - endLine) * lineHeight;
|
|
||||||
var arr = new Array(visibleCount + 2);
|
|
||||||
arr[0] = '<div style="height:' + topPadding + 'px"></div>';
|
|
||||||
for (var i = 0; i < visibleCount; i++) arr[i + 1] = '<div class="line-num">' + (startLine + i + 1) + '</div>';
|
|
||||||
arr[visibleCount + 1] = '<div style="height:' + bottomPadding + 'px"></div>';
|
|
||||||
ML.lineNumbers.innerHTML = arr.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Cursor Position =====
|
|
||||||
ML.updateCursorPosition = function () {
|
|
||||||
var text = ML.editor.value;
|
|
||||||
var pos = ML.editor.selectionStart;
|
|
||||||
var textBefore = text.substring(0, pos);
|
|
||||||
var lines = textBefore.split('\n');
|
|
||||||
ML.statusCursor.textContent = '行 ' + lines.length + ', 列 ' + (lines[lines.length - 1].length + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== File Size =====
|
|
||||||
ML.updateFileSize = function () {
|
|
||||||
var bytes = new Blob([ML.editor.value]).size;
|
|
||||||
ML.statusSize.textContent = ML.formatBytes(bytes);
|
|
||||||
ML.statusSizeDivider.classList.add('visible');
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Scroll Sync =====
|
|
||||||
var previewPositions = null;
|
|
||||||
var lineToPreviewMap = null;
|
|
||||||
|
|
||||||
ML.cachePreviewPositions = function () {
|
|
||||||
previewPositions = [];
|
|
||||||
for (var i = 0; i < ML.preview.children.length; i++) previewPositions.push(ML.preview.children[i].offsetTop);
|
|
||||||
buildLineToPreviewMap();
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildLineToPreviewMap() {
|
|
||||||
if (!previewPositions || previewPositions.length === 0) { lineToPreviewMap = null; return; }
|
|
||||||
var text = ML.editor.value;
|
|
||||||
var lines = text.split('\n');
|
|
||||||
var totalLines = lines.length;
|
|
||||||
var blockStartLines = [];
|
|
||||||
var inCodeBlock = false;
|
|
||||||
for (var i = 0; i < totalLines; i++) {
|
|
||||||
var trimmed = lines[i].trimStart();
|
|
||||||
if (trimmed.startsWith('```')) {
|
|
||||||
if (!inCodeBlock) { blockStartLines.push(i); inCodeBlock = true; }
|
|
||||||
else inCodeBlock = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inCodeBlock) continue;
|
|
||||||
if (trimmed.startsWith('#') || trimmed.startsWith('- ') || trimmed.startsWith('* ') || trimmed.startsWith('+ ') ||
|
|
||||||
/^\d+\.\s/.test(trimmed) || trimmed.startsWith('> ') || trimmed.startsWith('|') ||
|
|
||||||
trimmed.startsWith('---') || trimmed.startsWith('***') || trimmed.startsWith('___') ||
|
|
||||||
(trimmed === '' && i > 0 && lines[i - 1].trim() === '')) {
|
|
||||||
blockStartLines.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var uniqueStartsArr = [].concat(_toConsumableArray(new Set(blockStartLines))).sort(function (a, b) { return a - b; });
|
|
||||||
var uniqueStartsSet = new Set(uniqueStartsArr);
|
|
||||||
lineToPreviewMap = new Float32Array(totalLines);
|
|
||||||
if (uniqueStartsArr.length === 0) {
|
|
||||||
for (var _i = 0; _i < totalLines; _i++) lineToPreviewMap[_i] = (_i / Math.max(1, totalLines - 1)) * previewPositions[previewPositions.length - 1];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var domCount = previewPositions.length;
|
|
||||||
for (var _i2 = 0; _i2 < uniqueStartsArr.length; _i2++) {
|
|
||||||
var lineIdx = uniqueStartsArr[_i2];
|
|
||||||
var domIdx = Math.min(Math.floor((_i2 / uniqueStartsArr.length) * domCount), domCount - 1);
|
|
||||||
lineToPreviewMap[lineIdx] = previewPositions[domIdx];
|
|
||||||
}
|
|
||||||
for (var _i3 = 0; _i3 < totalLines; _i3++) {
|
|
||||||
if (lineToPreviewMap[_i3] !== 0 && uniqueStartsSet.has(_i3)) continue;
|
|
||||||
var prevLine = -1, nextLine = -1;
|
|
||||||
for (var j = _i3 - 1; j >= 0; j--) { if (lineToPreviewMap[j] !== 0 || uniqueStartsSet.has(j)) { prevLine = j; break; } }
|
|
||||||
for (var _j = _i3 + 1; _j < totalLines; _j++) { if (lineToPreviewMap[_j] !== 0 || uniqueStartsSet.has(_j)) { nextLine = _j; break; } }
|
|
||||||
if (prevLine === -1 && nextLine === -1) lineToPreviewMap[_i3] = 0;
|
|
||||||
else if (prevLine === -1) lineToPreviewMap[_i3] = lineToPreviewMap[nextLine];
|
|
||||||
else if (nextLine === -1) lineToPreviewMap[_i3] = lineToPreviewMap[prevLine];
|
|
||||||
else { var ratio = (_i3 - prevLine) / (nextLine - prevLine); lineToPreviewMap[_i3] = lineToPreviewMap[prevLine] + ratio * (lineToPreviewMap[nextLine] - lineToPreviewMap[prevLine]); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } return Array.from(arr); }
|
|
||||||
|
|
||||||
ML.syncScroll = function () {
|
|
||||||
if (ML.viewMode !== 'split') return;
|
|
||||||
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
|
|
||||||
var scrollTop = ML.editor.scrollTop;
|
|
||||||
var topLine = Math.floor(scrollTop / lineHeight);
|
|
||||||
if (!previewPositions || previewPositions.length === 0) ML.cachePreviewPositions();
|
|
||||||
if (!previewPositions || previewPositions.length === 0) return;
|
|
||||||
if (lineToPreviewMap && topLine >= 0 && topLine < lineToPreviewMap.length) {
|
|
||||||
ML.previewPanel.scrollTop = lineToPreviewMap[topLine];
|
|
||||||
} else {
|
|
||||||
var totalLines = ML.editor.value.split('\n').length;
|
|
||||||
var ratio = totalLines > 1 ? topLine / (totalLines - 1) : 0;
|
|
||||||
var targetIndex = Math.min(Math.floor(ratio * previewPositions.length), previewPositions.length - 1);
|
|
||||||
if (targetIndex >= 0 && previewPositions[targetIndex] !== undefined) ML.previewPanel.scrollTop = previewPositions[targetIndex];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Schedule Update =====
|
|
||||||
ML.scheduleUpdate = function () {
|
|
||||||
if (ML.updateTimer) clearTimeout(ML.updateTimer);
|
|
||||||
ML.updateTimer = setTimeout(function () {
|
|
||||||
ML.renderMarkdown(ML.editor.value);
|
|
||||||
ML.updateLineNumbers();
|
|
||||||
ML.updateFileSize();
|
|
||||||
ML.cachePreviewPositions();
|
|
||||||
}, 150);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
+1
-6
@@ -210,11 +210,6 @@
|
|||||||
|
|
||||||
<script src="../lib/marked.min.js"></script>
|
<script src="../lib/marked.min.js"></script>
|
||||||
<script src="../lib/highlight.min.js"></script>
|
<script src="../lib/highlight.min.js"></script>
|
||||||
<script src="markdown.js"></script>
|
<script src="renderer.js"></script>
|
||||||
<script src="editor.js"></script>
|
|
||||||
<script src="tabs.js"></script>
|
|
||||||
<script src="search.js"></script>
|
|
||||||
<script src="sidebar.js"></script>
|
|
||||||
<script src="core.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
// MarkLite — Markdown Module: marked.js init, sanitizer, rendering, preview links
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
var ML = window.MarkLite;
|
|
||||||
|
|
||||||
// ===== HTML Sanitizer =====
|
|
||||||
var DANGEROUS_TAGS = new Set(['script', 'iframe', 'object', 'embed', 'applet', 'form', 'base', 'meta', 'link', 'style']);
|
|
||||||
var DANGEROUS_ATTRS = new Set([
|
|
||||||
'onabort', 'onanimationend', 'onanimationiteration', 'onanimationstart', 'onauxclick', 'onbeforecopy', 'onbeforecut',
|
|
||||||
'onbeforepaste', 'onblur', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncontextmenu',
|
|
||||||
'oncopy', 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover',
|
|
||||||
'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'onfocusin', 'onfocusout',
|
|
||||||
'onfullscreenchange', 'onfullscreenerror', 'ongotpointercapture', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress',
|
|
||||||
'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onlostpointercapture', 'onmousedown',
|
|
||||||
'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onpaste',
|
|
||||||
'onpause', 'onplay', 'onplaying', 'onpointercancel', 'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove',
|
|
||||||
'onpointerout', 'onpointerover', 'onpointerup', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll',
|
|
||||||
'onsearch', 'onseeked', 'onseeking', 'onselect', 'onselectionchange', 'onselectstart', 'onshow', 'onstalled', 'onsubmit',
|
|
||||||
'onsuspend', 'ontimeupdate', 'ontoggle', 'ontouchcancel', 'ontouchend', 'ontouchmove', 'ontouchstart', 'ontransitionend',
|
|
||||||
'onvolumechange', 'onwaiting', 'onwheel', 'onanimationend', 'oncontentvisibilityautostatechange', 'onscrollend',
|
|
||||||
'onscrollsnapchange', 'onscrollsnapchanging'
|
|
||||||
]);
|
|
||||||
|
|
||||||
function sanitizeHTML(html) {
|
|
||||||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
|
||||||
var walker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT);
|
|
||||||
var toRemove = [];
|
|
||||||
while (walker.nextNode()) {
|
|
||||||
var el = walker.currentNode;
|
|
||||||
var tag = el.tagName.toLowerCase();
|
|
||||||
if (DANGEROUS_TAGS.has(tag)) { toRemove.push(el); continue; }
|
|
||||||
var attrs = Array.from(el.attributes);
|
|
||||||
for (var i = 0; i < attrs.length; i++) {
|
|
||||||
var name = attrs[i].name.toLowerCase();
|
|
||||||
if (DANGEROUS_ATTRS.has(name) || name.startsWith('on')) el.removeAttribute(attrs[i].name);
|
|
||||||
else if ((name === 'href' || name === 'action' || name === 'formaction' || name === 'xlink:href') && /^\s*javascript:/i.test(attrs[i].value)) el.removeAttribute(attrs[i].name);
|
|
||||||
else if (name === 'style' && /expression\s*\(|url\s*\(\s*['"]?\s*javascript:/i.test(attrs[i].value)) el.removeAttribute(attrs[i].name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var _i = 0; _i < toRemove.length; _i++) toRemove[_i].parentNode.removeChild(toRemove[_i]);
|
|
||||||
return doc.body.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Rendering =====
|
|
||||||
ML.renderMarkdown = function (text) {
|
|
||||||
if (typeof marked === 'undefined') { ML.preview.textContent = '错误: Markdown 解析库未加载'; ML.preview.style.color = 'red'; return; }
|
|
||||||
try {
|
|
||||||
var raw = marked.parse(text);
|
|
||||||
ML.preview.innerHTML = sanitizeHTML(raw);
|
|
||||||
ML.preview.style.color = '';
|
|
||||||
} catch (e) { ML.preview.textContent = '渲染错误: ' + e.message; ML.preview.style.color = 'red'; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Init marked.js =====
|
|
||||||
ML.initMarked = function () {
|
|
||||||
if (typeof marked === 'undefined') return;
|
|
||||||
var renderer = new marked.Renderer();
|
|
||||||
renderer.code = function (codeOrToken, legacyLang) {
|
|
||||||
var text = (typeof codeOrToken === 'object' && codeOrToken !== null) ? codeOrToken.text : codeOrToken;
|
|
||||||
var lang = (typeof codeOrToken === 'object' && codeOrToken !== null) ? codeOrToken.lang : legacyLang;
|
|
||||||
var highlighted = text;
|
|
||||||
if (typeof hljs !== 'undefined') {
|
|
||||||
if (lang && hljs.getLanguage(lang)) { try { highlighted = hljs.highlight(text, { language: lang }).value; } catch (e) { /* */ } }
|
|
||||||
else { try { highlighted = hljs.highlightAuto(text).value; } catch (e) { /* */ } }
|
|
||||||
}
|
|
||||||
var langClass = lang ? ' language-' + lang : '';
|
|
||||||
return '<pre><code class="hljs' + langClass + '">' + highlighted + '</code></pre>';
|
|
||||||
};
|
|
||||||
renderer.image = function (hrefOrToken, title, text) {
|
|
||||||
var href = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.href : hrefOrToken;
|
|
||||||
var alt = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.text : text;
|
|
||||||
var titleAttr = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.title : title;
|
|
||||||
var src = href;
|
|
||||||
if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:') && !src.startsWith('file://')) {
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
if (tab && tab.filePath) {
|
|
||||||
var dir = tab.filePath.replace(/[/\\][^/\\]+$/, '');
|
|
||||||
var sep = dir.includes('\\') ? '\\' : '/';
|
|
||||||
src = 'file://' + (dir + sep + src).replace(/\\/g, '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var titleStr = titleAttr ? ' title="' + titleAttr + '"' : '';
|
|
||||||
return '<img src="' + src + '" alt="' + alt + '"' + titleStr + '>';
|
|
||||||
};
|
|
||||||
marked.setOptions({ gfm: true, breaks: true, pedantic: false });
|
|
||||||
marked.use({ renderer: renderer });
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Preview Links =====
|
|
||||||
ML.initPreviewLinks = function () {
|
|
||||||
ML.preview.addEventListener('click', function (e) {
|
|
||||||
var link = e.target.closest('a');
|
|
||||||
if (!link) return;
|
|
||||||
e.preventDefault();
|
|
||||||
var href = link.getAttribute('href');
|
|
||||||
if (!href) return;
|
|
||||||
if (href.startsWith('#')) { var target = ML.preview.querySelector(href); if (target) target.scrollIntoView({ behavior: 'smooth' }); return; }
|
|
||||||
try { var url = new URL(href); if (url.protocol !== 'http:' && url.protocol !== 'https:') return; } catch (err) { return; }
|
|
||||||
if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openExternal) window.electronAPI.openExternal(href);
|
|
||||||
else window.open(href, '_blank', 'noopener');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,158 +0,0 @@
|
|||||||
// MarkLite — Search & Replace Module
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
var ML = window.MarkLite;
|
|
||||||
|
|
||||||
var searchMatches = [];
|
|
||||||
var searchIndex = -1;
|
|
||||||
var searchCaseSensitive = false;
|
|
||||||
var searchUseRegex = false;
|
|
||||||
|
|
||||||
ML.openSearch = function (showReplace) {
|
|
||||||
ML.searchBar.classList.remove('hidden');
|
|
||||||
ML.replaceRow.classList.toggle('hidden', !showReplace);
|
|
||||||
ML.searchInput.focus();
|
|
||||||
var selected = ML.editor.value.substring(ML.editor.selectionStart, ML.editor.selectionEnd);
|
|
||||||
if (selected && !selected.includes('\n')) ML.searchInput.value = selected;
|
|
||||||
doSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.closeSearch = function () {
|
|
||||||
ML.searchBar.classList.add('hidden');
|
|
||||||
clearHighlights();
|
|
||||||
searchMatches = []; searchIndex = -1;
|
|
||||||
ML.searchCount.textContent = '';
|
|
||||||
ML.editor.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.toggleSearchOption = function (type) {
|
|
||||||
if (type === 'case') { searchCaseSensitive = !searchCaseSensitive; ML.btnCase.classList.toggle('active', searchCaseSensitive); }
|
|
||||||
else { searchUseRegex = !searchUseRegex; ML.btnRegex.classList.toggle('active', searchUseRegex); }
|
|
||||||
doSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
function doSearch() {
|
|
||||||
clearHighlights();
|
|
||||||
var text = ML.searchInput.value;
|
|
||||||
if (!text) { searchMatches = []; searchIndex = -1; ML.searchCount.textContent = ''; return; }
|
|
||||||
searchMatches = findMatches(text);
|
|
||||||
if (searchMatches.length > 0) {
|
|
||||||
var cursor = ML.editor.selectionStart;
|
|
||||||
searchIndex = searchMatches.findIndex(function (m) { return m.start >= cursor; });
|
|
||||||
if (searchIndex === -1) searchIndex = 0;
|
|
||||||
scrollToMatch(searchIndex);
|
|
||||||
} else { searchIndex = -1; }
|
|
||||||
ML.searchCount.textContent = searchMatches.length > 0 ? (searchIndex + 1) + '/' + searchMatches.length : '无结果';
|
|
||||||
highlightMatches();
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMatches(text) {
|
|
||||||
var content = ML.editor.value;
|
|
||||||
var matches = [];
|
|
||||||
if (searchUseRegex) {
|
|
||||||
try {
|
|
||||||
var flags = searchCaseSensitive ? 'g' : 'gi';
|
|
||||||
var regex = new RegExp(text, flags);
|
|
||||||
var m;
|
|
||||||
while ((m = regex.exec(content)) !== null) {
|
|
||||||
matches.push({ start: m.index, end: m.index + m[0].length });
|
|
||||||
if (m[0].length === 0) regex.lastIndex++;
|
|
||||||
}
|
|
||||||
} catch (e) { /* invalid regex */ }
|
|
||||||
} else {
|
|
||||||
var haystack = searchCaseSensitive ? content : content.toLowerCase();
|
|
||||||
var needle = searchCaseSensitive ? text : text.toLowerCase();
|
|
||||||
var idx = 0;
|
|
||||||
while ((idx = haystack.indexOf(needle, idx)) !== -1) {
|
|
||||||
matches.push({ start: idx, end: idx + needle.length });
|
|
||||||
idx += needle.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlightMatches() {
|
|
||||||
clearHighlights();
|
|
||||||
if (searchMatches.length === 0) return;
|
|
||||||
var content = ML.editor.value;
|
|
||||||
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
|
|
||||||
var editorStyle = getComputedStyle(ML.editor);
|
|
||||||
var paddingTop = parseFloat(editorStyle.paddingTop);
|
|
||||||
var paddingLeft = parseFloat(editorStyle.paddingLeft);
|
|
||||||
var lineStarts = [0];
|
|
||||||
for (var i = 0; i < content.length; i++) { if (content.charCodeAt(i) === 10) lineStarts.push(i + 1); }
|
|
||||||
function posToXY(pos) {
|
|
||||||
var line = 0;
|
|
||||||
for (var _i = 0; _i < lineStarts.length; _i++) { if (lineStarts[_i] > pos) break; line = _i; }
|
|
||||||
return { top: line * lineHeight + paddingTop, left: (pos - lineStarts[line]) * ML.getCharWidth() + paddingLeft };
|
|
||||||
}
|
|
||||||
var overlay = document.createElement('div');
|
|
||||||
overlay.id = 'search-highlights';
|
|
||||||
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;z-index:1;';
|
|
||||||
var wrapper = document.getElementById('editor-wrapper');
|
|
||||||
wrapper.style.position = 'relative';
|
|
||||||
wrapper.appendChild(overlay);
|
|
||||||
for (var _i2 = 0; _i2 < searchMatches.length; _i2++) {
|
|
||||||
var m = searchMatches[_i2];
|
|
||||||
var startPos = posToXY(m.start);
|
|
||||||
var endPos = posToXY(m.end);
|
|
||||||
var isCurrent = _i2 === searchIndex;
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.className = 'search-hl' + (isCurrent ? ' current' : '');
|
|
||||||
div.style.cssText = 'position:absolute;top:' + startPos.top + 'px;left:' + startPos.left + 'px;height:' + lineHeight + 'px;width:' + Math.max(endPos.left - startPos.left, 8) + 'px;';
|
|
||||||
overlay.appendChild(div);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearHighlights() { var old = document.getElementById('search-highlights'); if (old) old.remove(); }
|
|
||||||
|
|
||||||
function scrollToMatch(index) {
|
|
||||||
if (index < 0 || index >= searchMatches.length) return;
|
|
||||||
searchIndex = index;
|
|
||||||
var m = searchMatches[index];
|
|
||||||
ML.editor.setSelectionRange(m.start, m.end);
|
|
||||||
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
|
|
||||||
var content = ML.editor.value;
|
|
||||||
var line = 0;
|
|
||||||
for (var i = 0; i < m.start; i++) { if (content.charCodeAt(i) === 10) line++; }
|
|
||||||
var matchTop = line * lineHeight;
|
|
||||||
var viewTop = ML.editor.scrollTop;
|
|
||||||
var viewBottom = viewTop + ML.editor.clientHeight;
|
|
||||||
if (matchTop < viewTop + 40 || matchTop > viewBottom - 40) ML.editor.scrollTop = matchTop - ML.editor.clientHeight / 2;
|
|
||||||
ML.searchCount.textContent = (index + 1) + '/' + searchMatches.length;
|
|
||||||
highlightMatches();
|
|
||||||
}
|
|
||||||
|
|
||||||
ML.findNext = function () { if (searchMatches.length === 0) return; scrollToMatch((searchIndex + 1) % searchMatches.length); };
|
|
||||||
ML.findPrev = function () { if (searchMatches.length === 0) return; scrollToMatch((searchIndex - 1 + searchMatches.length) % searchMatches.length); };
|
|
||||||
|
|
||||||
ML.replaceCurrent = function () {
|
|
||||||
if (searchMatches.length === 0 || searchIndex < 0) return;
|
|
||||||
var m = searchMatches[searchIndex];
|
|
||||||
ML.editor.setSelectionRange(m.start, m.end);
|
|
||||||
document.execCommand('insertText', false, ML.replaceInput.value);
|
|
||||||
doSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.replaceAll = function () {
|
|
||||||
if (searchMatches.length === 0) return;
|
|
||||||
var replacement = ML.replaceInput.value;
|
|
||||||
for (var i = searchMatches.length - 1; i >= 0; i--) {
|
|
||||||
var m = searchMatches[i];
|
|
||||||
ML.editor.setSelectionRange(m.start, m.end);
|
|
||||||
document.execCommand('insertText', false, replacement);
|
|
||||||
}
|
|
||||||
doSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.initSearchBar = function () {
|
|
||||||
ML.searchInput.addEventListener('input', doSearch);
|
|
||||||
ML.btnCase.addEventListener('click', function () { ML.toggleSearchOption('case'); });
|
|
||||||
ML.btnRegex.addEventListener('click', function () { ML.toggleSearchOption('regex'); });
|
|
||||||
ML.btnNext.addEventListener('click', ML.findNext);
|
|
||||||
ML.btnPrev.addEventListener('click', ML.findPrev);
|
|
||||||
ML.btnSearchClose.addEventListener('click', ML.closeSearch);
|
|
||||||
ML.btnReplace.addEventListener('click', ML.replaceCurrent);
|
|
||||||
ML.btnReplaceAll.addEventListener('click', ML.replaceAll);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
// MarkLite — Sidebar Module: file tree, independent files, resize handle
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
var ML = window.MarkLite;
|
|
||||||
|
|
||||||
var expandedDirs = new Set();
|
|
||||||
var lastTreeData = null;
|
|
||||||
|
|
||||||
// ===== Sidebar Highlight =====
|
|
||||||
ML.updateSidebarHighlight = function () {
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
var activePath = tab ? tab.filePath : null;
|
|
||||||
if (!ML.currentRootPath) {
|
|
||||||
ML.sidebarTree.innerHTML = '';
|
|
||||||
var indep = renderIndependentFiles();
|
|
||||||
if (indep) ML.sidebarTree.appendChild(indep);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var oldSection = ML.sidebarTree.querySelector('.independent-files-section');
|
|
||||||
var newSection = renderIndependentFiles();
|
|
||||||
if (oldSection) oldSection.remove();
|
|
||||||
if (newSection) ML.sidebarTree.insertBefore(newSection, ML.sidebarTree.firstChild);
|
|
||||||
var treeItems = ML.sidebarTree.querySelectorAll('.tree-item:not(.independent-file-item)');
|
|
||||||
treeItems.forEach(function (item) {
|
|
||||||
var itemPath = item.dataset.path;
|
|
||||||
if (itemPath) item.classList.toggle('active', itemPath === activePath);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Independent Files =====
|
|
||||||
function getIndependentFiles() {
|
|
||||||
if (!ML.tabs.length) return [];
|
|
||||||
return ML.tabs.filter(function (t) {
|
|
||||||
if (!t.filePath) return false;
|
|
||||||
if (!ML.currentRootPath) return true;
|
|
||||||
return !t.filePath.startsWith(ML.currentRootPath);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderIndependentFiles() {
|
|
||||||
var files = getIndependentFiles();
|
|
||||||
if (files.length === 0) return null;
|
|
||||||
var section = document.createElement('div');
|
|
||||||
section.className = 'independent-files-section';
|
|
||||||
var header = document.createElement('div');
|
|
||||||
header.className = 'independent-files-header';
|
|
||||||
header.textContent = '已打开的文件';
|
|
||||||
section.appendChild(header);
|
|
||||||
files.forEach(function (tab) {
|
|
||||||
var item = document.createElement('div');
|
|
||||||
item.className = 'tree-item independent-file-item';
|
|
||||||
item.style.paddingLeft = '8px';
|
|
||||||
item.dataset.path = tab.filePath;
|
|
||||||
var icon = document.createElement('span');
|
|
||||||
icon.className = 'tree-icon';
|
|
||||||
icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>';
|
|
||||||
item.appendChild(icon);
|
|
||||||
var name = document.createElement('span');
|
|
||||||
name.className = 'tree-name';
|
|
||||||
name.textContent = ML.getFileName(tab.filePath);
|
|
||||||
item.appendChild(name);
|
|
||||||
if (tab.isModified) {
|
|
||||||
var dot = document.createElement('span');
|
|
||||||
dot.className = 'independent-modified-dot';
|
|
||||||
dot.textContent = ' •';
|
|
||||||
item.appendChild(dot);
|
|
||||||
}
|
|
||||||
if (tab.id === ML.activeTabId) item.classList.add('active');
|
|
||||||
item.addEventListener('click', function (e) { e.stopPropagation(); ML.switchToTab(tab.id); });
|
|
||||||
section.appendChild(item);
|
|
||||||
});
|
|
||||||
return section;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Folder Operations =====
|
|
||||||
async function openFolderDialog() {
|
|
||||||
if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openFolderDialog) {
|
|
||||||
var result = await window.electronAPI.openFolderDialog();
|
|
||||||
if (result) loadFolder(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFolder(dirPath) {
|
|
||||||
if (typeof window.electronAPI === 'undefined') return;
|
|
||||||
ML.currentRootPath = dirPath;
|
|
||||||
var result = await window.electronAPI.readDirTree(dirPath);
|
|
||||||
if (result.success) {
|
|
||||||
renderTree(result.tree, dirPath);
|
|
||||||
ML.sidebar.classList.remove('collapsed');
|
|
||||||
window.electronAPI.watchDir(dirPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshTree() {
|
|
||||||
if (!ML.currentRootPath) return;
|
|
||||||
var result = await window.electronAPI.readDirTree(ML.currentRootPath);
|
|
||||||
if (result.success) renderTree(result.tree, ML.currentRootPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Tree Rendering =====
|
|
||||||
function renderTreeOriginal(tree, rootPath) {
|
|
||||||
ML.sidebarTree.innerHTML = '';
|
|
||||||
var rootNode = { name: ML.getFileName(rootPath) || rootPath, path: rootPath, type: 'dir', children: tree };
|
|
||||||
expandedDirs.add(rootPath);
|
|
||||||
var fragment = document.createDocumentFragment();
|
|
||||||
buildTreeDOM(fragment, rootNode, 0);
|
|
||||||
ML.sidebarTree.appendChild(fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTree(tree, rootPath) {
|
|
||||||
lastTreeData = tree;
|
|
||||||
renderTreeOriginal(tree, rootPath);
|
|
||||||
var folderHeader = document.createElement('div');
|
|
||||||
folderHeader.className = 'sidebar-section-header';
|
|
||||||
folderHeader.textContent = '文件夹目录树';
|
|
||||||
ML.sidebarTree.insertBefore(folderHeader, ML.sidebarTree.firstChild);
|
|
||||||
var indepSection = renderIndependentFiles();
|
|
||||||
if (indepSection) ML.sidebarTree.insertBefore(indepSection, ML.sidebarTree.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTreeDOM(parent, node, depth) {
|
|
||||||
var item = document.createElement('div');
|
|
||||||
item.className = 'tree-item';
|
|
||||||
item.style.paddingLeft = (8 + depth * 16) + 'px';
|
|
||||||
if (node.type === 'dir') {
|
|
||||||
var isExpanded = expandedDirs.has(node.path);
|
|
||||||
var arrow = document.createElement('span');
|
|
||||||
arrow.className = 'tree-arrow' + (isExpanded ? ' expanded' : '');
|
|
||||||
arrow.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"></polyline></svg>';
|
|
||||||
item.appendChild(arrow);
|
|
||||||
var icon = document.createElement('span');
|
|
||||||
icon.className = 'tree-icon';
|
|
||||||
icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
|
|
||||||
item.appendChild(icon);
|
|
||||||
var name = document.createElement('span');
|
|
||||||
name.className = 'tree-name';
|
|
||||||
name.textContent = node.name;
|
|
||||||
item.appendChild(name);
|
|
||||||
parent.appendChild(item);
|
|
||||||
if (isExpanded && node.children) {
|
|
||||||
node.children.forEach(function (child) { buildTreeDOM(parent, child, depth + 1); });
|
|
||||||
}
|
|
||||||
item.addEventListener('click', function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (expandedDirs.has(node.path)) expandedDirs.delete(node.path);
|
|
||||||
else expandedDirs.add(node.path);
|
|
||||||
renderTree(lastTreeData || [], ML.currentRootPath);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var spacer = document.createElement('span');
|
|
||||||
spacer.style.width = '16px';
|
|
||||||
spacer.style.flexShrink = '0';
|
|
||||||
item.appendChild(spacer);
|
|
||||||
var fileIcon = document.createElement('span');
|
|
||||||
fileIcon.className = 'tree-icon';
|
|
||||||
fileIcon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>';
|
|
||||||
item.appendChild(fileIcon);
|
|
||||||
var fileName = document.createElement('span');
|
|
||||||
fileName.className = 'tree-name';
|
|
||||||
fileName.textContent = node.name;
|
|
||||||
item.appendChild(fileName);
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
if (tab && tab.filePath === node.path) item.classList.add('active');
|
|
||||||
item.dataset.path = node.path;
|
|
||||||
parent.appendChild(item);
|
|
||||||
item.addEventListener('click', async function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
var existing = ML.tabs.find(function (t) { return t.filePath === node.path; });
|
|
||||||
if (existing) { ML.switchToTab(existing.id); }
|
|
||||||
else if (typeof window.electronAPI !== 'undefined') {
|
|
||||||
var result = await window.electronAPI.readFile(node.path);
|
|
||||||
if (result.success) ML.createNewTab(node.path, result.content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Init =====
|
|
||||||
ML.initSidebar = function () {
|
|
||||||
var settings = ML.loadSettings();
|
|
||||||
if (settings.sidebarCollapsed) ML.sidebar.classList.add('collapsed');
|
|
||||||
if (settings.sidebarWidth) ML.sidebar.style.width = settings.sidebarWidth + 'px';
|
|
||||||
|
|
||||||
// Resize handle
|
|
||||||
var handle = document.createElement('div');
|
|
||||||
handle.className = 'sidebar-resize-handle';
|
|
||||||
ML.sidebar.appendChild(handle);
|
|
||||||
var isResizing = false;
|
|
||||||
handle.addEventListener('mousedown', function (e) { isResizing = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); });
|
|
||||||
document.addEventListener('mousemove', function (e) {
|
|
||||||
if (!isResizing) return;
|
|
||||||
var newWidth = Math.max(180, Math.min(500, e.clientX));
|
|
||||||
ML.sidebar.style.width = newWidth + 'px';
|
|
||||||
ML.saveSetting('sidebarWidth', newWidth);
|
|
||||||
});
|
|
||||||
document.addEventListener('mouseup', function () { if (isResizing) { isResizing = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; } });
|
|
||||||
|
|
||||||
ML.btnOpenFolder.addEventListener('click', function () { if (typeof window.electronAPI !== 'undefined') openFolderDialog(); });
|
|
||||||
if (typeof window.electronAPI !== 'undefined') {
|
|
||||||
var dirRefreshTimer = null;
|
|
||||||
window.electronAPI.onDirChanged(function () {
|
|
||||||
if (!ML.currentRootPath) return;
|
|
||||||
if (dirRefreshTimer) clearTimeout(dirRefreshTimer);
|
|
||||||
dirRefreshTimer = setTimeout(function () { refreshTree(); }, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
// MarkLite — Tabs Module
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
var ML = window.MarkLite;
|
|
||||||
|
|
||||||
// ===== Tab State =====
|
|
||||||
ML.tabs = [];
|
|
||||||
ML.activeTabId = null;
|
|
||||||
var tabIdCounter = 0;
|
|
||||||
ML.mruTabStack = [];
|
|
||||||
|
|
||||||
ML.createTab = function (filePath, content) {
|
|
||||||
var id = ++tabIdCounter;
|
|
||||||
return { id: id, filePath: filePath || null, content: content || '', isModified: false, scrollTop: 0, scrollLeft: 0, selectionStart: 0, selectionEnd: 0, previewScrollTop: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.getActiveTab = function () { return ML.tabs.find(function (t) { return t.id === ML.activeTabId; }) || null; };
|
|
||||||
ML.getTabIndex = function (tabId) { return ML.tabs.findIndex(function (t) { return t.id === tabId; }); };
|
|
||||||
|
|
||||||
ML.saveCurrentTabState = function () {
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
if (!tab) return;
|
|
||||||
tab.content = ML.editor.value;
|
|
||||||
tab.scrollTop = ML.editor.scrollTop;
|
|
||||||
tab.scrollLeft = ML.editor.scrollLeft;
|
|
||||||
tab.selectionStart = ML.editor.selectionStart;
|
|
||||||
tab.selectionEnd = ML.editor.selectionEnd;
|
|
||||||
tab.previewScrollTop = ML.previewPanel.scrollTop;
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.loadTabState = function (tab) {
|
|
||||||
ML.editor.focus(); ML.editor.select();
|
|
||||||
var inserted = document.execCommand('insertText', false, tab.content);
|
|
||||||
if (!inserted) ML.editor.value = tab.content;
|
|
||||||
ML.lineCountCache = 0;
|
|
||||||
ML.updateLineNumbers();
|
|
||||||
ML.renderMarkdown(tab.content);
|
|
||||||
ML.updateFileSize();
|
|
||||||
ML.cachePreviewPositions();
|
|
||||||
requestAnimationFrame(function () {
|
|
||||||
ML.editor.scrollTop = tab.scrollTop;
|
|
||||||
ML.editor.scrollLeft = tab.scrollLeft;
|
|
||||||
ML.lineNumbers.scrollTop = tab.scrollTop;
|
|
||||||
ML.editor.setSelectionRange(tab.selectionStart, tab.selectionEnd);
|
|
||||||
ML.previewPanel.scrollTop = tab.previewScrollTop;
|
|
||||||
ML.updateCursorPosition();
|
|
||||||
});
|
|
||||||
ML.setModified(tab.isModified);
|
|
||||||
if (typeof window.electronAPI !== 'undefined') window.electronAPI.tabSwitched(tab.filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.setModified = function (modified) {
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
if (tab) {
|
|
||||||
tab.isModified = modified;
|
|
||||||
var tabEl = ML.tabList.querySelector('.tab-item[data-tab-id="' + tab.id + '"]');
|
|
||||||
if (tabEl) tabEl.classList.toggle('modified', modified);
|
|
||||||
}
|
|
||||||
ML.updateTitle();
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.createNewTab = function (filePath, content) {
|
|
||||||
if (filePath) {
|
|
||||||
var existing = ML.tabs.find(function (t) { return t.filePath === filePath; });
|
|
||||||
if (existing) { ML.switchToTab(existing.id); return existing; }
|
|
||||||
}
|
|
||||||
var tab = ML.createTab(filePath, content);
|
|
||||||
ML.tabs.push(tab);
|
|
||||||
ML.switchToTab(tab.id);
|
|
||||||
return tab;
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.switchToTab = function (tabId) {
|
|
||||||
if (tabId === ML.activeTabId) return;
|
|
||||||
ML.saveCurrentTabState();
|
|
||||||
if (ML.activeTabId) {
|
|
||||||
ML.mruTabStack = ML.mruTabStack.filter(function (id) { return id !== ML.activeTabId; });
|
|
||||||
ML.mruTabStack.unshift(ML.activeTabId);
|
|
||||||
}
|
|
||||||
ML.activeTabId = tabId;
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
if (!tab) return;
|
|
||||||
ML.loadTabState(tab);
|
|
||||||
ML.renderTabBar();
|
|
||||||
ML.updateTitle();
|
|
||||||
ML.updateSidebarHighlight();
|
|
||||||
ML.welcomeScreen.classList.add('hidden');
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.closeTab = function (tabId) {
|
|
||||||
var index = ML.getTabIndex(tabId);
|
|
||||||
if (index === -1) return;
|
|
||||||
var tab = ML.tabs[index];
|
|
||||||
if (tab.isModified) {
|
|
||||||
var name = tab.filePath ? ML.getFileName(tab.filePath) : '未命名';
|
|
||||||
if (!confirm('"' + name + '" 尚未保存,确定要关闭吗?')) return;
|
|
||||||
}
|
|
||||||
ML.tabs.splice(index, 1);
|
|
||||||
ML.mruTabStack = ML.mruTabStack.filter(function (id) { return id !== tabId; });
|
|
||||||
if (tabId === ML.activeTabId) {
|
|
||||||
if (ML.tabs.length === 0) {
|
|
||||||
ML.activeTabId = null;
|
|
||||||
ML.editor.value = '';
|
|
||||||
ML.preview.innerHTML = '';
|
|
||||||
ML.lineNumbers.innerHTML = '';
|
|
||||||
ML.lineCountCache = 0;
|
|
||||||
ML.statusSize.textContent = '';
|
|
||||||
ML.statusSizeDivider.classList.remove('visible');
|
|
||||||
ML.welcomeScreen.classList.remove('hidden');
|
|
||||||
document.title = 'MarkLite';
|
|
||||||
ML.statusText.textContent = '就绪';
|
|
||||||
if (typeof window.electronAPI !== 'undefined') window.electronAPI.tabSwitched(null);
|
|
||||||
} else {
|
|
||||||
var newIndex = Math.min(index, ML.tabs.length - 1);
|
|
||||||
ML.activeTabId = ML.tabs[newIndex].id;
|
|
||||||
ML.loadTabState(ML.tabs[newIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ML.renderTabBar();
|
|
||||||
ML.updateTitle();
|
|
||||||
ML.updateSidebarHighlight();
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.renderTabBar = function () {
|
|
||||||
ML.tabList.innerHTML = '';
|
|
||||||
ML.tabs.forEach(function (tab) {
|
|
||||||
var el = document.createElement('div');
|
|
||||||
el.className = 'tab-item' + (tab.id === ML.activeTabId ? ' active' : '') + (tab.isModified ? ' modified' : '');
|
|
||||||
el.dataset.tabId = tab.id;
|
|
||||||
var name = document.createElement('span');
|
|
||||||
name.className = 'tab-name';
|
|
||||||
name.textContent = tab.filePath ? ML.getFileName(tab.filePath) : '未命名';
|
|
||||||
el.appendChild(name);
|
|
||||||
var 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', function (e) { e.stopPropagation(); ML.closeTab(tab.id); });
|
|
||||||
el.appendChild(close);
|
|
||||||
el.addEventListener('click', function () { ML.switchToTab(tab.id); });
|
|
||||||
ML.tabList.appendChild(el);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ML.updateTitle = function () {
|
|
||||||
var tab = ML.getActiveTab();
|
|
||||||
if (tab) {
|
|
||||||
var name = tab.filePath ? ML.getFileName(tab.filePath) : '未命名';
|
|
||||||
var mod = tab.isModified ? ' *' : '';
|
|
||||||
document.title = 'MarkLite - ' + name + mod;
|
|
||||||
ML.statusText.textContent = name;
|
|
||||||
} else {
|
|
||||||
document.title = 'MarkLite';
|
|
||||||
ML.statusText.textContent = '就绪';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user