From 2bab893e808cc85df2acdea2e5e573c4c5ee80f1 Mon Sep 17 00:00:00 2001 From: mixflavor <31689802+mixflavor@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:20:09 +0900 Subject: [PATCH] =?UTF-8?q?marktile=200.0.49=20=E2=80=94=20fix=20Cmd+Z=20u?= =?UTF-8?q?ndo=20in=20the=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editor's Cmd+Z relied on the contenteditable's native undo (wiped by the re-highlight); now wired to the same snapshot stack the toolbar undo uses. Synced from the tile monorepo. Co-Authored-By: chodaict --- main.js | 35 +++++++++++++++++++++++++++++------ manifest.json | 2 +- versions.json | 2 +- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/main.js b/main.js index 279f8c1..f90c257 100644 --- a/main.js +++ b/main.js @@ -65,13 +65,22 @@ function highlightLineParts(line) { if (hm) cls += ' tg-h tg-h' + hm[1].length; else if (/^>\s?/.test(line)) cls += ' tg-quote'; else if (/^\s*[-*]\s/.test(line)) cls += ' tg-li'; + // Each syntax marker (## , > , - , [ ], **, *, ~~, `, [[ ]], @{}) is wrapped in its own + // so a host can hide JUST the markers via CSS (.tugtile-preview .tg-mk{display:none}) while the styling stays — + // the basis for a marker-free preview look. tg-mk is transparent to the text round-trip (getText reads + // textContent, which still includes the marker chars). The Obsidian plugins never add .tugtile-preview, so + // markers stay visible there (their 調味/原味 cycle is unchanged); only a host that opts in hides them. escHtml + // has already turned a leading > into > (the quote marker), so match that form. const h = escHtml(line) - .replace(/^(\s*[-*]\s)(\[[ xX]\])/, (m, p, box) => p + '' + box + '') - .replace(/(\*\*[^*\n]+\*\*|\*[^*\s][^*\n]*?\*)/g, (m) => '' + m + '') // **bold** wins the alternation; single * needs a non-space after it so "a * b" isn't italicised - .replace(/(~~[^~\n]+~~)/g, '$1') - .replace(/(`[^`\n]+`)/g, '$1') - .replace(/(\[\[[^\]\n]+\]\])/g, '$1') - .replace(/(@@?\{[^}\n]*\})/g, '$1') + .replace(/^(#{1,6}\s)/, '$1') // heading marker + .replace(/^(>\s?)/, '$1') // blockquote marker + .replace(/^(\s*[-*]\s)(\[[ xX]\])/, (m, p, box) => '' + p + '' + box + '') + .replace(/^(\s*[-*]\s)/, '$1') // plain bullet (heading/quote/checkbox lines already start with a , so this won't match them) + .replace(/(\*\*[^*\n]+\*\*|\*[^*\s][^*\n]*?\*)/g, (m) => { const mk = m.startsWith('**') ? '**' : '*'; return '' + mk + '' + m.slice(mk.length, m.length - mk.length) + '' + mk + ''; }) // **bold** wins the alternation; single * needs a non-space after it so "a * b" isn't italicised + .replace(/(~~[^~\n]+~~)/g, (m) => '~~' + m.slice(2, -2) + '~~') + .replace(/(`[^`\n]+`)/g, (m) => '`' + m.slice(1, -1) + '`') + .replace(/(\[\[[^\]\n]+\]\])/g, (m) => '[[' + m.slice(2, -2) + ']]') + .replace(/(@@?\{)([^}\n]*)(\})/g, (m, op, inner, cl) => '' + op + '' + inner + '' + cl + '') .replace(/(^|[^&\w])(#[^\s#<&]+)/g, '$1$2') .replace(/\t/g, '\t'); // wrap each literal tab LAST (after the line-start regexes) so CSS can mark tab-vs-space; span is transparent to the text round-trip if (/^\s*[-*]\s\[[ xX]\]/.test(line)) cls += ' tg-task' + (/^\s*[-*]\s\[[xX]\]/.test(line) ? ' tg-task-done' : ''); @@ -428,6 +437,20 @@ function mountEditor(contentEl, opts, host) { host.attachDatePicker(ta); ta.addEventListener('keydown', (e) => { if (host.isSubmitKey(e)) { e.preventDefault(); if (opts.onSubmit) opts.onSubmit(); return; } + // Undo/redo via OUR snapshot stack (same as the toolbar buttons). The editor rebuilds innerHTML on every + // re-highlight, which wipes the contenteditable's native undo — so Cmd+Z must NOT rely on the browser's + // native undo (that's why it silently stopped working). preventDefault blocks native; stopPropagation keeps + // the board's document-level ⌘Z handler out; the read-only guard mirrors the toolbar (locked = no edits). + if ((e.metaKey || e.ctrlKey) && (e.key || '').toLowerCase() === 'z') { + e.preventDefault(); e.stopPropagation(); + if (ed.getAttribute('contenteditable') !== 'false') { if (e.shiftKey) runs.redo(); else runs.undo(); } + return; + } + if ((e.metaKey || e.ctrlKey) && (e.key || '').toLowerCase() === 'y') { // ⌘/Ctrl+Y = redo (Windows convention) + e.preventDefault(); e.stopPropagation(); + if (ed.getAttribute('contenteditable') !== 'false') runs.redo(); + return; + } if (e.key === 'Escape' && opts.onEscape) { e.preventDefault(); opts.onEscape(); return; } // hosts without a cancel action (marktile) let Escape fall through naturally if (e.key === 'Enter' && !e.isComposing && e.keyCode !== 229 && tryListContinue()) e.preventDefault(); // newline-producing Enter (submit already handled above) → continue the list if on one if (e.key === 'Tab' && !e.isComposing) { // insert/remove a literal tab (contenteditable's default Tab just moves focus) diff --git a/manifest.json b/manifest.json index 75440a1..ba81564 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "marktile", "name": "marktile", - "version": "0.0.48", + "version": "0.0.49", "minAppVersion": "1.4.0", "description": "A Markdown editor where the markers never hide — headings grow while ## stays put. Pairs with tugtile. CJK-friendly.", "author": "CVER Inc.", diff --git a/versions.json b/versions.json index efd1671..04d71ba 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "0.0.48": "1.4.0" + "0.0.49": "1.4.0" }