diff --git a/src/core.js b/src/core.js index a7bb169..ee8f3c8 100644 --- a/src/core.js +++ b/src/core.js @@ -229,6 +229,9 @@ class MarkdownEditor { textarea.style.tabSize = String(this.config.tabSize || 2); textarea.placeholder = this.config.placeholder || i18nT('placeholder'); textarea.setAttribute('aria-label', i18nT('edit')); + if (this.config.syntaxHighlight) { + textarea.classList.add('me-textarea-highlight'); + } editorInner.appendChild(textarea); editorPane.appendChild(editorInner); diff --git a/src/plugins.js b/src/plugins.js index 7e6a1b5..fbb9700 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -600,102 +600,94 @@ const shortcutHelpPlugin = { }; /** - * 文件系统存储插件(v0.1.14) - * 使用 IndexedDB 持久化编辑器内容 + * 文件系统插件(v0.1.14) + * 使用 File System Access API 读写磁盘文件 */ const fileSystemPlugin = { name: 'fileSystem', version: '0.1.0', - description: 'IndexedDB 文件系统存储', + description: '磁盘文件读写(File System Access API)', priority: 90, - dbName: 'metona-editor-fs', - storeName: 'documents', - _db: null, - - _openDB() { - if (this._db) return Promise.resolve(this._db); - if (typeof indexedDB === 'undefined') return Promise.reject(new Error('IndexedDB not supported')); - return new Promise((resolve, reject) => { - const req = indexedDB.open(this.dbName, 1); - req.onupgradeneeded = (e) => { - const db = e.target.result; - if (!db.objectStoreNames.contains(this.storeName)) { - db.createObjectStore(this.storeName, { keyPath: 'id' }); - } - }; - req.onsuccess = (e) => { this._db = e.target.result; resolve(this._db); }; - req.onerror = () => reject(req.error); - }); - }, + _fileHandle: null, install(editor) { - if (!editor || typeof indexedDB === 'undefined') return; - if (typeof editor.getValue !== 'function') return; + if (!editor || typeof editor.getValue !== 'function') return; - editor.saveToFile = async (docId) => { - const id = docId || (editor.id || 'default'); - const content = editor.getValue(); + // 检查 API 支持 + const hasAPI = typeof window !== 'undefined' && typeof window.showOpenFilePicker === 'function'; + + /** 打开磁盘文件 */ + editor.openFile = async (opts = {}) => { + if (!hasAPI) { + editor.toast?.('File System Access API 不支持此浏览器', { type: 'warning' }); + return null; + } try { - const db = await this._openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction(this.storeName, 'readwrite'); - tx.objectStore(this.storeName).put({ id, content, updatedAt: Date.now() }); - tx.oncomplete = () => resolve(true); - tx.onerror = () => reject(tx.error); + const [handle] = await window.showOpenFilePicker({ + types: [{ accept: { 'text/markdown': ['.md', '.txt', '.markdown'] } }], + ...opts, }); - } catch (e) { console.error('FileSystem save error:', e); return false; } + this._fileHandle = handle; + const file = await handle.getFile(); + const content = await file.text(); + editor.setValue(content); + editor._emit?.('fileOpened', { name: file.name, handle }); + return { name: file.name, content, handle }; + } catch (e) { + if (e.name !== 'AbortError') console.error('Open file error:', e); + return null; + } }; - editor.loadFromFile = async (docId) => { - const id = docId || (editor.id || 'default'); + /** 保存到当前文件(如已打开)或弹出另存为对话框 */ + editor.saveFile = async (opts = {}) => { + let handle = this._fileHandle; + if (!handle || opts.saveAs) { + if (!hasAPI) { + editor.toast?.('File System Access API 不支持此浏览器', { type: 'warning' }); + return false; + } + try { + handle = await window.showSaveFilePicker({ + types: [{ accept: { 'text/markdown': ['.md'] } }], + suggestedName: opts.name || 'document.md', + }); + this._fileHandle = handle; + } catch (e) { + if (e.name !== 'AbortError') console.error('Save file error:', e); + return false; + } + } try { - const db = await this._openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction(this.storeName, 'readonly'); - const req = tx.objectStore(this.storeName).get(id); - req.onsuccess = () => { - if (req.result) { editor.setValue(req.result.content, { silent: true }); } - resolve(req.result ? req.result.content : null); - }; - req.onerror = () => reject(req.error); - }); - } catch (e) { console.error('FileSystem load error:', e); return null; } + const writable = await handle.createWritable(); + await writable.write(editor.getValue()); + await writable.close(); + editor._emit?.('fileSaved', { handle }); + return true; + } catch (e) { + // 权限过期,清除句柄重试 + this._fileHandle = null; + if (!opts.saveAs) return editor.saveFile({ ...opts, saveAs: true }); + console.error('Write file error:', e); + return false; + } }; - editor.deleteFile = async (docId) => { - const id = docId || (editor.id || 'default'); - try { - const db = await this._openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction(this.storeName, 'readwrite'); - tx.objectStore(this.storeName).delete(id); - tx.oncomplete = () => resolve(true); - tx.onerror = () => reject(tx.error); - }); - } catch (e) { return false; } - }; + /** 另存为新文件 */ + editor.saveFileAs = (name) => editor.saveFile({ saveAs: true, name }); - editor.listFiles = async () => { - try { - const db = await this._openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction(this.storeName, 'readonly'); - const req = tx.objectStore(this.storeName).getAll(); - req.onsuccess = () => resolve(req.result || []); - req.onerror = () => reject(req.error); - }); - } catch (e) { return []; } - }; + /** 获取当前文件句柄 */ + editor.getFileHandle = () => this._fileHandle; }, destroy(editor) { - if (this._db) { this._db.close(); this._db = null; } + this._fileHandle = null; if (editor) { - delete editor.saveToFile; - delete editor.loadFromFile; - delete editor.deleteFile; - delete editor.listFiles; + delete editor.openFile; + delete editor.saveFile; + delete editor.saveFileAs; + delete editor.getFileHandle; } }, }; diff --git a/src/styles.js b/src/styles.js index 68ecdf0..c435fdc 100644 --- a/src/styles.js +++ b/src/styles.js @@ -134,11 +134,11 @@ const generateCSS = () => { .me-editor-inner { display: flex; height: 100%; overflow: hidden; position: relative; } /* 语法高亮叠加层(v0.1.14) */ .me-highlight-layer { - position: absolute; top: 0; left: 0; right: 0; bottom: 0; - padding: 16px 18px; margin: 0; border: 0; outline: 0; overflow: auto; + position: absolute; top: 0; left: 36px; right: 0; bottom: 0; + padding: 16px 18px; margin: 0; border: 0; outline: 0; overflow: hidden; font-family: var(--md-mono); font-size: 13.5px; line-height: 1.7; white-space: pre-wrap; word-wrap: break-word; - color: transparent; pointer-events: none; background: transparent; + pointer-events: none; background: transparent; } .mh-heading { color: var(--md-accent); font-weight: 700; } .mh-bold { font-weight: 700; } @@ -204,6 +204,18 @@ const generateCSS = () => { display: block; white-space: pre-wrap; word-wrap: break-word; + position: relative; + z-index: 1; + } + /* v0.1.14: 语法高亮模式 — textarea 透明,高亮层显示在下方 */ + .me-textarea-highlight { + color: transparent !important; + caret-color: var(--md-text); + background: transparent !important; + } + .me-textarea-highlight::selection { + background: rgba(59, 130, 246, 0.25); + color: transparent; } .me-textarea::placeholder { color: var(--md-muted); opacity: 0.7; } .me-textarea:focus { outline: 0; }