class MarkdownEditor { constructor() { this.initializeElements(); this.initializeEventListeners(); this.initializeAutoSave(); this.initializeToolbar(); this.initializeDragAndDrop(); this.initializeLocalBackup(); this.lastSaveTime = null; } initializeElements() { this.titleInput = document.getElementById('titleInput'); this.contentInput = document.getElementById('contentInput'); this.preview = document.getElementById('preview'); this.imageInput = document.getElementById('imageInput'); this.saveButton = document.querySelector('.save-button'); this.wordCount = document.querySelector('.word-count'); this.toolbar = document.querySelector('.editor-toolbar'); this.isDirty = false; this.lastSavedContent = ''; this.autoSaveInterval = null; // Clear localStorage if this is a new article const isNewArticle = window.location.pathname.endsWith('/editor'); if (isNewArticle) { localStorage.removeItem('editor-content'); localStorage.removeItem('editor-title'); // Clear inputs for new article this.titleInput.value = ''; this.contentInput.value = ''; } } initializeEventListeners() { this.contentInput.addEventListener('input', () => { this.handleContentChange(); this.updateWordCount(); this.isDirty = true; }); this.titleInput.addEventListener('input', () => { this.isDirty = true; }); this.imageInput.addEventListener('change', (event) => { this.handleImageUpload(event); }); this.contentInput.addEventListener('keydown', (event) => { this.handleShortcuts(event); }); this.saveButton.addEventListener('click', () => { this.saveArticle(); }); window.addEventListener('beforeunload', (event) => { if (this.isDirty) { event.preventDefault(); event.returnValue = '您有未保存的更改,确定要离开吗?'; } }); } handleContentChange() { this.updatePreview(); } updatePreview() { try { const content = this.contentInput.value; const markedInstance = marked.parse || marked; this.preview.innerHTML = markedInstance(content, { breaks: true, gfm: true, highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; } return code; } }); this.preview.querySelectorAll('pre code').forEach(block => { hljs.highlightElement(block); }); } catch (error) { console.error('预览渲染错误:', error); this.preview.innerHTML = '
预览渲染失败
'; } } initializeAutoSave() { this.autoSaveInterval = setInterval(() => { if (this.isDirty && this.lastSaveTime && (Date.now() - this.lastSaveTime) >= 600000) { // 10 minutes this.autoSave(); } }, 60000); // Check every minute } async autoSave() { if (!this.isDirty) return; const content = this.contentInput.value; const title = this.titleInput.value; if (content === this.lastSavedContent || !title.trim() || !content.trim()) { return; } try { const response = await this.saveArticle(true); if (response && response.ok) { this.lastSavedContent = content; this.showNotification('自动保存成功', 'success'); } } catch (error) { console.error('自动保存失败:', error); this.showNotification('自动保存失败', 'error'); } } insertText(before, after, defaultText = '') { const start = this.contentInput.selectionStart; const end = this.contentInput.selectionEnd; const content = this.contentInput.value; const selectedText = content.substring(start, end) || defaultText; const replacement = before + selectedText + after; this.contentInput.value = content.substring(0, start) + replacement + content.substring(end); this.contentInput.focus(); const newCursorPos = start + before.length + selectedText.length; this.contentInput.setSelectionRange(newCursorPos, newCursorPos); this.updatePreview(); this.isDirty = true; } async handleImageUpload(event) { const file = event.target.files[0]; if (!file) return; if (!file.type.startsWith('image/')) { this.showNotification('请选择图片文件', 'error'); return; } const formData = new FormData(); formData.append('file', file); try { this.showNotification('正在上传图片...', 'info'); const response = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await response.json(); if (data.url) { this.insertText(`![${file.name}](${data.url})`, ''); this.showNotification('图片上传成功', 'success'); } else { throw new Error(data.error || '上传失败'); } } catch (error) { console.error('图片上传错误:', error); this.showNotification('图片上传失败', 'error'); } } handleShortcuts(event) { if (event.ctrlKey || event.metaKey) { switch (event.key.toLowerCase()) { case 's': event.preventDefault(); this.saveArticle(); break; case 'b': event.preventDefault(); this.insertText('**', '**', '粗体文本'); break; case 'i': event.preventDefault(); this.insertText('*', '*', '斜体文本'); break; } } } async saveArticle(isAutoSave = false) { const title = this.titleInput.value.trim(); const content = this.contentInput.value.trim(); this.lastSaveTime = Date.now(); if (!title || !content) { this.showNotification('标题和内容不能为空', 'error'); return; } const articleSlug = window.location.pathname.split('/').pop(); const isEdit = articleSlug !== 'editor'; try { if (!isAutoSave) this.showNotification('正在保存...', 'info'); const response = await fetch(`/api/articles${isEdit ? '/' + articleSlug : ''}`, { method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content }) }); const data = await response.json(); if (response.ok) { this.isDirty = false; if (!isAutoSave) { this.showNotification('保存成功', 'success'); window.location.href = `/article/${data.slug || articleSlug}`; } return response; } else { throw new Error(data.error || '保存失败'); } } catch (error) { console.error('保存文章错误:', error); this.showNotification(error.message, 'error'); throw error; } } updateWordCount() { const content = this.contentInput.value; const wordCount = content.length; if (this.wordCount) { this.wordCount.textContent = `字数:${wordCount}`; } } showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = `
${message}
`; document.body.appendChild(notification); const closeButton = notification.querySelector('.notification-close'); closeButton.addEventListener('click', () => { notification.remove(); }); setTimeout(() => { notification.classList.add('fade-out'); setTimeout(() => { notification.remove(); }, 300); }, 3000); } initializeToolbar() { const tools = [ { name: 'bold', icon: '', title: '粗体 (Ctrl+B)', action: () => this.insertText('**', '**', '粗体文本') }, { name: 'italic', icon: '', title: '斜体 (Ctrl+I)', action: () => this.insertText('*', '*', '斜体文本') }, { name: 'heading1', icon: '', title: '一级标题', action: () => this.insertText('\n# ', '', '标题') }, { name: 'heading2', icon: '', title: '二级标题', action: () => this.insertText('\n## ', '', '标题') }, { name: 'code', icon: '', title: '代码块', action: () => this.insertText('\n```\n', '\n```\n', '在此输入代码') }, { name: 'link', icon: '', title: '链接', action: () => this.insertText('[', '](https://)', '链接文本') }, { name: 'image', icon: '', title: '图片', action: () => this.imageInput.click() }, { name: 'list', icon: '', title: '无序列表', action: () => this.insertText('\n- ', '', '列表项') }, { name: 'numbered-list', icon: '', title: '有序列表', action: () => this.insertText('\n1. ', '', '列表项') }, { name: 'quote', icon: '', title: '引用', action: () => this.insertText('\n> ', '', '引用文本') }, { name: 'divider', icon: '', title: '分隔线', action: () => this.insertText('\n---\n', '', '') } ]; tools.forEach(tool => { const button = document.createElement('button'); button.className = `toolbar-button ${tool.name}`; button.innerHTML = tool.icon; button.title = tool.title; button.addEventListener('click', (e) => { e.preventDefault(); tool.action(); }); this.toolbar.appendChild(button); }); } initializeDragAndDrop() { const dropZone = this.contentInput; ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }); }); ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, () => { dropZone.classList.add('drag-over'); }); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, () => { dropZone.classList.remove('drag-over'); }); }); dropZone.addEventListener('drop', (e) => { const files = e.dataTransfer.files; if (files.length > 0 && files[0].type.startsWith('image/')) { this.imageInput.files = files; this.handleImageUpload({ target: this.imageInput }); } }); } initializeLocalBackup() { // Only restore from local storage if we're editing an existing article const isNewArticle = window.location.pathname.endsWith('/editor'); if (!isNewArticle) { const savedContent = localStorage.getItem('editor-content'); const savedTitle = localStorage.getItem('editor-title'); if (savedContent && !this.contentInput.value) { this.contentInput.value = savedContent; this.updatePreview(); } if (savedTitle && !this.titleInput.value) { this.titleInput.value = savedTitle; } } // Save to local storage periodically setInterval(() => { if (this.isDirty) { localStorage.setItem('editor-content', this.contentInput.value); localStorage.setItem('editor-title', this.titleInput.value); } }, 10000); } destroy() { clearInterval(this.autoSaveInterval); localStorage.removeItem('editor-content'); localStorage.removeItem('editor-title'); } } // Initialize editor when the DOM is ready document.addEventListener('DOMContentLoaded', () => { const editor = new MarkdownEditor(); });