fix(v0.1.14): disk file system, syntax highlight transparency, zero-dep Mermaid
fix(plugins): rewrite fileSystem to use File System Access API - openFile(): showOpenFilePicker for real .md disk files - saveFile(): showSaveFilePicker or write to existing handle - saveFileAs(): force save-as dialog - Graceful fallback when API unsupported fix(core): syntax highlight layer transparency - textarea gets me-textarea-highlight class when syntaxHighlight enabled - CSS: color transparent, caret-color visible, selection bg visible - Highlight layer positioned after gutter (left:36px) verify(parser): Mermaid self-implemented container - Creates <div class="me-mermaid"> with <pre class="mermaid"> - Zero third-party dependencies required - Users optionally load Mermaid.js to initialize rendering
This commit is contained in:
@@ -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);
|
||||
|
||||
+66
-74
@@ -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 {
|
||||
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);
|
||||
handle = await window.showSaveFilePicker({
|
||||
types: [{ accept: { 'text/markdown': ['.md'] } }],
|
||||
suggestedName: opts.name || 'document.md',
|
||||
});
|
||||
} catch (e) { console.error('FileSystem load error:', e); return null; }
|
||||
this._fileHandle = handle;
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') console.error('Save file error:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+15
-3
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user