diff --git a/index.html b/index.html
index b1bfdbe..53575ba 100644
--- a/index.html
+++ b/index.html
@@ -307,7 +307,7 @@
@@ -373,15 +373,15 @@
Menu
diff --git a/script.js b/script.js
index a724848..a74971a 100644
--- a/script.js
+++ b/script.js
@@ -56,6 +56,16 @@ document.addEventListener("DOMContentLoaded", function () {
let activeFindIndex = -1;
let lastFindQuery = '';
+ // Custom Editor History State Manager variables
+ const tabHistories = {};
+ let currentHistoryTabId = null;
+ let lastPushedValue = '';
+ let typingTimeout = null;
+ let lastInputType = null; // 'insert', 'delete', 'programmatic', or null
+ let lastCursorStart = 0;
+ let lastCursorEnd = 0;
+ let pendingState = null;
+
const markdownEditor = document.getElementById("markdown-editor");
const markdownPreview = document.getElementById("markdown-preview");
const markdownFormatToolbar = document.getElementById("markdown-format-toolbar");
@@ -1270,11 +1280,26 @@ document.addEventListener("DOMContentLoaded", function () {
function switchTab(tabId) {
if (tabId === activeTabId) return;
saveCurrentTabState();
+
+ // Clear typing timeout and reset tracking for the new tab
+ if (typingTimeout) {
+ clearTimeout(typingTimeout);
+ typingTimeout = null;
+ }
+ lastInputType = null;
+ pendingState = null;
+
activeTabId = tabId;
saveActiveTabId(activeTabId);
const tab = tabs.find(function(t) { return t.id === tabId; });
if (!tab) return;
markdownEditor.value = tab.content;
+
+ initTabHistory(tabId, tab.content);
+ lastPushedValue = tab.content;
+ currentHistoryTabId = tabId;
+ updateUndoRedoButtons();
+
restoreViewMode(tab.viewMode);
renderMarkdown();
requestAnimationFrame(function() {
@@ -1299,6 +1324,12 @@ document.addEventListener("DOMContentLoaded", function () {
function closeTab(tabId) {
const idx = tabs.findIndex(function(t) { return t.id === tabId; });
if (idx === -1) return;
+
+ // Clean up history of the closed tab
+ if (tabHistories[tabId]) {
+ delete tabHistories[tabId];
+ }
+
tabs.splice(idx, 1);
if (tabs.length === 0) {
// Auto-create new "Untitled" when last tab is deleted
@@ -1465,6 +1496,8 @@ document.addEventListener("DOMContentLoaded", function () {
}
const activeTab = tabs.find(function(t) { return t.id === activeTabId; });
markdownEditor.value = activeTab.content;
+ initTabHistory(activeTabId, activeTab.content);
+ updateUndoRedoButtons();
restoreViewMode(activeTab.viewMode);
renderMarkdown();
const editorPane = document.querySelector('.editor-pane');
@@ -2382,12 +2415,15 @@ document.addEventListener("DOMContentLoaded", function () {
}
function replaceEditorRange(start, end, replacement, selectStart, selectEnd) {
+ pushProgrammaticHistoryState();
markdownEditor.focus();
markdownEditor.setRangeText(replacement, start, end, 'end');
const nextStart = typeof selectStart === 'number' ? selectStart : start + replacement.length;
const nextEnd = typeof selectEnd === 'number' ? selectEnd : nextStart;
markdownEditor.setSelectionRange(nextStart, nextEnd);
markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+ lastPushedValue = markdownEditor.value;
+ lastInputType = 'programmatic';
}
function wrapEditorSelection(prefix, suffix, placeholder) {
@@ -2591,6 +2627,286 @@ document.addEventListener("DOMContentLoaded", function () {
.replace(/`([^`]+)`/g, '$1');
}
+ function getOrCreateTabHistory(tabId) {
+ if (!tabId) return { undoStack: [], redoStack: [] };
+ if (!tabHistories[tabId]) {
+ tabHistories[tabId] = {
+ undoStack: [],
+ redoStack: []
+ };
+ }
+ return tabHistories[tabId];
+ }
+
+ function initTabHistory(tabId, initialValue) {
+ const hist = getOrCreateTabHistory(tabId);
+ if (hist.undoStack.length === 0) {
+ hist.undoStack.push({
+ value: initialValue || '',
+ selectionStart: 0,
+ selectionEnd: 0
+ });
+ lastPushedValue = initialValue || '';
+ currentHistoryTabId = tabId;
+ pendingState = null;
+ }
+ }
+
+ function pushProgrammaticHistoryState() {
+ if (typingTimeout) {
+ clearTimeout(typingTimeout);
+ typingTimeout = null;
+ }
+
+ const tabId = activeTabId;
+ const hist = getOrCreateTabHistory(tabId);
+ const currentValue = markdownEditor.value;
+
+ if (pendingState) {
+ hist.undoStack.push(pendingState);
+ if (hist.undoStack.length > 200) {
+ hist.undoStack.shift();
+ }
+ hist.redoStack.length = 0;
+ pendingState = null;
+ lastPushedValue = currentValue;
+ } else if (currentValue !== lastPushedValue) {
+ hist.undoStack.push({
+ value: currentValue,
+ selectionStart: markdownEditor.selectionStart,
+ selectionEnd: markdownEditor.selectionEnd
+ });
+ if (hist.undoStack.length > 200) {
+ hist.undoStack.shift();
+ }
+ hist.redoStack.length = 0;
+ lastPushedValue = currentValue;
+ }
+ updateUndoRedoButtons();
+ }
+
+ function commitPendingState() {
+ if (typingTimeout) {
+ clearTimeout(typingTimeout);
+ typingTimeout = null;
+ }
+ if (!pendingState) return;
+
+ const tabId = activeTabId;
+ const hist = getOrCreateTabHistory(tabId);
+
+ hist.undoStack.push(pendingState);
+ if (hist.undoStack.length > 200) {
+ hist.undoStack.shift();
+ }
+
+ hist.redoStack.length = 0;
+ lastPushedValue = markdownEditor.value;
+ pendingState = null;
+ updateUndoRedoButtons();
+ }
+
+ function handleKeystrokeHistory(e) {
+ const currentValue = markdownEditor.value;
+ if (currentValue === lastPushedValue) return;
+
+ const inputType = e ? e.inputType : '';
+
+ if (!pendingState) {
+ pendingState = {
+ value: lastPushedValue,
+ selectionStart: lastCursorStart,
+ selectionEnd: lastCursorEnd
+ };
+ }
+
+ let shouldCommit = false;
+
+ if (inputType === 'insertLineBreak' || inputType === 'insertParagraph' || inputType === 'insertFromPaste' || lastInputType === 'programmatic') {
+ shouldCommit = true;
+ } else if (e && e.data === ' ') {
+ shouldCommit = true;
+ } else {
+ const isDelete = inputType.startsWith('delete');
+ const wasDelete = lastInputType === 'delete';
+ const isInsert = inputType.startsWith('insert');
+ const wasInsert = lastInputType === 'insert';
+
+ if ((isDelete && wasInsert) || (isInsert && wasDelete)) {
+ shouldCommit = true;
+ }
+ }
+
+ if (shouldCommit) {
+ commitPendingState();
+ }
+
+ if (typingTimeout) {
+ clearTimeout(typingTimeout);
+ }
+ typingTimeout = setTimeout(function() {
+ commitPendingState();
+ }, 1000);
+
+ if (inputType.startsWith('delete')) {
+ lastInputType = 'delete';
+ } else if (inputType.startsWith('insert')) {
+ lastInputType = 'insert';
+ } else {
+ lastInputType = 'other';
+ }
+ }
+
+ function updateLastCursor() {
+ if (markdownEditor) {
+ lastCursorStart = markdownEditor.selectionStart;
+ lastCursorEnd = markdownEditor.selectionEnd;
+ }
+ }
+
+ function updateUndoRedoButtons() {
+ const undoBtn = document.querySelector('[data-md-action="undo"]');
+ const redoBtn = document.querySelector('[data-md-action="redo"]');
+ if (!undoBtn || !redoBtn) return;
+
+ const tabId = activeTabId;
+ const hist = getOrCreateTabHistory(tabId);
+
+ const canUndo = hist.undoStack.length > 0 || pendingState !== null;
+ const canRedo = hist.redoStack.length > 0;
+
+ undoBtn.disabled = !canUndo;
+ undoBtn.classList.toggle('disabled', !canUndo);
+
+ redoBtn.disabled = !canRedo;
+ redoBtn.classList.toggle('disabled', !canRedo);
+ }
+
+ function executeUndo() {
+ if (typingTimeout) {
+ clearTimeout(typingTimeout);
+ typingTimeout = null;
+ }
+
+ const tabId = activeTabId;
+ const hist = getOrCreateTabHistory(tabId);
+ const currentValue = markdownEditor.value;
+
+ let stateToRestore = null;
+
+ if (pendingState) {
+ stateToRestore = pendingState;
+ pendingState = null;
+
+ hist.redoStack.push({
+ value: currentValue,
+ selectionStart: markdownEditor.selectionStart,
+ selectionEnd: markdownEditor.selectionEnd
+ });
+ if (hist.redoStack.length > 200) {
+ hist.redoStack.shift();
+ }
+ } else if (hist.undoStack.length > 0) {
+ const topState = hist.undoStack.pop();
+ if (topState) {
+ stateToRestore = topState;
+
+ hist.redoStack.push({
+ value: currentValue,
+ selectionStart: markdownEditor.selectionStart,
+ selectionEnd: markdownEditor.selectionEnd
+ });
+ if (hist.redoStack.length > 200) {
+ hist.redoStack.shift();
+ }
+ }
+ }
+
+ if (stateToRestore) {
+ markdownEditor.value = stateToRestore.value;
+ markdownEditor.setSelectionRange(stateToRestore.selectionStart, stateToRestore.selectionEnd);
+ lastPushedValue = stateToRestore.value;
+ lastInputType = null;
+
+ markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+ saveCurrentTabState();
+ }
+
+ updateUndoRedoButtons();
+ }
+
+ function executeRedo() {
+ if (typingTimeout) {
+ clearTimeout(typingTimeout);
+ typingTimeout = null;
+ }
+
+ const tabId = activeTabId;
+ const hist = getOrCreateTabHistory(tabId);
+ const currentValue = markdownEditor.value;
+
+ if (hist.redoStack.length > 0) {
+ const stateToRestore = hist.redoStack.pop();
+
+ hist.undoStack.push({
+ value: currentValue,
+ selectionStart: markdownEditor.selectionStart,
+ selectionEnd: markdownEditor.selectionEnd
+ });
+ if (hist.undoStack.length > 200) {
+ hist.undoStack.shift();
+ }
+
+ markdownEditor.value = stateToRestore.value;
+ markdownEditor.setSelectionRange(stateToRestore.selectionStart, stateToRestore.selectionEnd);
+ lastPushedValue = stateToRestore.value;
+ lastInputType = null;
+ pendingState = null;
+
+ markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+ saveCurrentTabState();
+ }
+
+ updateUndoRedoButtons();
+ }
+
+ function stripMarkdownFormatting(text) {
+ if (!text) return '';
+ return text
+ // Remove fenced code block syntax
+ .replace(/^```[a-zA-Z0-9-]*\r?\n?/gm, '')
+ .replace(/```\r?$/gm, '')
+ // Remove reference link definitions (e.g., [id]: url "title")
+ .replace(/^\[[^\]]+\]:\s*\S+(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*$/gm, '')
+ // Strip basic markdown constructs (headers, blockquotes, lists, bold, italic, strikethrough, code)
+ .replace(/^#{1,6}\s+/gm, '')
+ .replace(/^>\s?/gm, '')
+ .replace(/^(\s*)([-*+]|\d+\.)\s+/gm, '$1')
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
+ // HTML alignment tags or custom tags (strip the tags, keep inner text)
+ .replace(/<[^>]+>/g, '')
+ // Bold, Italic, Strikethrough, Inline code
+ .replace(/(\*\*|__)(.*?)\1/g, '$2')
+ .replace(/(\*|_)(.*?)\1/g, '$2')
+ .replace(/~~(.*?)~~/g, '$1')
+ .replace(/`([^`]+)`/g, '$1')
+ // Remove horizontal rules
+ .replace(/^\s*[-*_]{3,}\s*$/gm, '');
+ }
+
+ function applyClearFormatting() {
+ const fullText = markdownEditor.value;
+ pushProgrammaticHistoryState();
+ replaceEditorRange(0, fullText.length, '', 0, 0);
+
+ // Force immediate visual rendering and gutter update
+ renderMarkdown();
+ updateLineNumbers();
+ updateFindHighlights();
+ saveCurrentTabState();
+ }
+
function toTitleCase(text) {
return text.toLowerCase().replace(/\b\w/g, function(letter) {
return letter.toUpperCase();
@@ -4788,10 +5104,12 @@ document.addEventListener("DOMContentLoaded", function () {
}
function runMarkdownTool(action, button) {
- if (action === 'undo' || action === 'redo') {
- markdownEditor.focus();
- document.execCommand(action);
- markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+ if (action === 'undo') {
+ executeUndo();
+ return;
+ }
+ if (action === 'redo') {
+ executeRedo();
return;
}
@@ -5123,7 +5441,8 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
- markdownEditor.addEventListener("input", function() {
+ markdownEditor.addEventListener("input", function(e) {
+ handleKeystrokeHistory(e);
debouncedRender();
clearTimeout(saveTabStateTimeout);
saveTabStateTimeout = setTimeout(saveCurrentTabState, 500);
@@ -5135,6 +5454,12 @@ document.addEventListener("DOMContentLoaded", function () {
scheduleLineNumberUpdate();
});
+ markdownEditor.addEventListener('keydown', updateLastCursor);
+ markdownEditor.addEventListener('keyup', updateLastCursor);
+ markdownEditor.addEventListener('mousedown', updateLastCursor);
+ markdownEditor.addEventListener('mouseup', updateLastCursor);
+ markdownEditor.addEventListener('focus', updateLastCursor);
+
initMarkdownFormatToolbar();
initFindReplaceModal();
initAppModals();
@@ -6551,6 +6876,19 @@ document.addEventListener("DOMContentLoaded", function () {
}
document.addEventListener("keydown", function (e) {
+ if (document.activeElement === markdownEditor) {
+ const isCmdOrCtrl = e.ctrlKey || e.metaKey;
+ if (isCmdOrCtrl && !e.shiftKey && e.key.toLowerCase() === 'z') {
+ e.preventDefault();
+ executeUndo();
+ return;
+ } else if ((isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === 'z') || (isCmdOrCtrl && e.key.toLowerCase() === 'y')) {
+ e.preventDefault();
+ executeRedo();
+ return;
+ }
+ }
+
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
exportMd.click();
diff --git a/styles.css b/styles.css
index fb88af6..b4814fa 100644
--- a/styles.css
+++ b/styles.css
@@ -693,6 +693,13 @@ body {
background-color: var(--button-active);
}
+.markdown-tool-btn:disabled,
+.markdown-tool-btn.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
.markdown-tool-btn i {
font-size: 15px;
}