@@ -307,7 +307,9 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
307307 < span style ="font-weight:bold; "> Changelogs</ span >
308308 < button id ="changelog-popup-close " style ="background:none;border:none;color:#fff;font-size:20px;cursor:pointer; "> ×</ button >
309309 </ div >
310- < iframe src ="https://objectpresents.github.io/lancer-notes/ " style ="width:100%;height:calc(100% - 44px);border:none;background:#fff; "> </ iframe >
310+ < div id ="changelog-content " class ="preview-content " style ="width:100%;height:calc(100% - 44px);border:none;overflow:auto;padding:16px; ">
311+ <!-- Local changelogs will be injected here via JS (parsed from markdown) -->
312+ </ div >
311313 </ div >
312314
313315< input type ="file " id ="file-input " class ="hidden-file-input " accept =".md,.txt,.text " />
@@ -353,11 +355,73 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
353355 let currentFile = null ;
354356 let undoStack = [ ] ;
355357 let redoStack = [ ] ;
358+ let lastUndoValue = '' ;
359+ let undoTimer = null ;
360+ const UNDO_STACK_LIMIT = 100 ;
361+ let lastUndoSaveTime = 0 ;
362+ let undoThrottleMs = 250 ; // Reduced throttle time for better responsiveness
356363 let currentViewMode = 'split' ;
357364
358365 // Version information
359- const CURRENT_VERSION = '2.1.1 ' ;
366+ const CURRENT_VERSION = '2.1.2 ' ;
360367 const GITHUB_RELEASES_API = 'https://api.github.com/repos/ObjectPresents/lancer-notes/releases/latest' ;
368+ // Local changelog markdown (sourced from README.md 'Changelogs' section)
369+ const LOCAL_CHANGELOG_MD = `
370+
371+ ## v2.1.2 (26/10/2025)
372+ - Small fixes and polish for v2.1.2:
373+ - Fix dark-mode styling for the undo/redo history dropdown
374+ - Keep history dropdown visible when hovering between the toolbar button and the menu
375+ - Move changelogs to a local view and ensure the popup respects dark mode
376+ - Improved undo/redo system with reduced lag (250ms throttle)
377+ - Fixed changelog formatting and removed duplicate entries
378+ - Renamed README.md to CHANGELOG.md for better organization
379+ - Bumped application version to v2.1.2
380+
381+ ## v2.1.1 (25/10/2025)
382+ No major features; maintenance and polish:
383+ - Added "Check for Updates" feature
384+
385+ ### v1.3.1 (25/10/2025)
386+ - Security patch (XSS) applied in v1.3.1 only.
387+ - Patched a Cross-Site Scripting (XSS) vulnerability that affected older code paths.
388+ - Security Mode was enabled as part of that v1.3.1 patch to mitigate risk.
389+
390+ ### v1.3 (25/10/2025)
391+ - Improvements and bug fixes for legacy apps.
392+ - Added Find and Replace.
393+
394+ ### v2.1 (23/10/2025)
395+ - New editor features and quality-of-life improvements.
396+ - HR, Task items, Table alignment, Subscript, Superscript, and more.
397+
398+ ### v2.0.5 (01/10/2025)
399+ - UI and layout updates; help/changelog layouts improved.
400+ - Various dark-mode fixes.
401+
402+ ### v2.0.4 (25/09/2025)
403+ - Popup UI changed to be modular and centered; escaping and list fixes.
404+
405+ ### v2.0.3 (24/09/2025)
406+ - Added regex search/replace with flags and preview highlighting.
407+
408+ ### v2.0.2 (21/09/2025)
409+ - Find and Replace support.
410+
411+ ### v2.0.1 (19/09/2025)
412+ - Added support for .txt files; fixed split-mode resize bug.
413+
414+ ### v2.0 (18/09/2025)
415+ - Major internal restructuring.
416+
417+ ### v1.2 (17/09/2025)
418+ - Fixed table formatting issues.
419+
420+ ### v1.1 (16/09/2025)
421+ - New icon design and minor bug fixes.
422+
423+ ### v1.0 (14/01/2024)
424+ - Original Markdown Editor (forked from Lancer Fan Club Forums).` ;
361425
362426 // Toggle dark mode
363427 function toggleDarkMode ( ) {
@@ -380,10 +444,10 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
380444 document . body . classList . add ( 'dark-mode' ) ;
381445 }
382446 // Set up event listeners
383- editor . addEventListener ( 'input' , function ( ) {
447+ editor . addEventListener ( 'input' , function ( e ) {
384448 updatePreview ( ) ;
385449 updateStatusBar ( ) ;
386- saveToUndoStack ( ) ;
450+ saveToUndoStack ( e ) ;
387451 } ) ;
388452
389453 editor . addEventListener ( 'keydown' , function ( e ) {
@@ -397,8 +461,16 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
397461 document . getElementById ( 'btn-new' ) . addEventListener ( 'click' , newFile ) ;
398462 document . getElementById ( 'btn-open' ) . addEventListener ( 'click' , openFile ) ;
399463 document . getElementById ( 'btn-save' ) . addEventListener ( 'click' , saveFile ) ;
400- document . getElementById ( 'btn-undo' ) . addEventListener ( 'click' , undo ) ;
401- document . getElementById ( 'btn-redo' ) . addEventListener ( 'click' , redo ) ;
464+ const undoBtn = document . getElementById ( 'btn-undo' ) ;
465+ const redoBtn = document . getElementById ( 'btn-redo' ) ;
466+ undoBtn . addEventListener ( 'click' , undo ) ;
467+ redoBtn . addEventListener ( 'click' , redo ) ;
468+ // Show history dropdown on hover
469+ undoBtn . addEventListener ( 'mouseenter' , function ( ) { showHistoryDropdown ( 'undo' , undoBtn ) ; } ) ;
470+ redoBtn . addEventListener ( 'mouseenter' , function ( ) { showHistoryDropdown ( 'redo' , redoBtn ) ; } ) ;
471+ // Remove dropdown on mouseleave (with small delay to allow click)
472+ undoBtn . addEventListener ( 'mouseleave' , function ( ) { setTimeout ( ( ) => { const d = document . getElementById ( 'undo-history-dropdown' ) ; if ( d ) d . remove ( ) ; } , 300 ) ; } ) ;
473+ redoBtn . addEventListener ( 'mouseleave' , function ( ) { setTimeout ( ( ) => { const d = document . getElementById ( 'redo-history-dropdown' ) ; if ( d ) d . remove ( ) ; } , 300 ) ; } ) ;
402474 document . getElementById ( 'btn-bold' ) . addEventListener ( 'click' , function ( ) { insertMarkdown ( '**' , '**' ) ; } ) ;
403475 document . getElementById ( 'btn-italic' ) . addEventListener ( 'click' , function ( ) { insertMarkdown ( '*' , '*' ) ; } ) ;
404476 document . getElementById ( 'btn-strike' ) . addEventListener ( 'click' , function ( ) { insertMarkdown ( '~~' , '~~' ) ; } ) ;
@@ -1167,12 +1239,144 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
11671239 }
11681240
11691241 // Undo/Redo functionality
1170- function saveToUndoStack ( ) {
1171- undoStack . push ( editor . value ) ;
1172- if ( undoStack . length > 50 ) {
1173- undoStack . shift ( ) ;
1242+ // Helper to summarize a state for dropdown
1243+ function summarizeState ( text ) {
1244+ let firstLine = text . split ( '\n' ) [ 0 ] ;
1245+ if ( firstLine . length > 40 ) firstLine = firstLine . slice ( 0 , 37 ) + '...' ;
1246+ return firstLine || '[Empty]' ;
1247+ }
1248+
1249+ // Create and show history dropdown
1250+ function showHistoryDropdown ( type , anchorBtn ) {
1251+ let stack = type === 'undo' ? undoStack : redoStack ;
1252+ if ( ! stack . length ) return ;
1253+ const maxItems = 15 ;
1254+ const items = stack . slice ( - maxItems ) . map ( summarizeState ) ;
1255+ let dropdown = document . getElementById ( type + '-history-dropdown' ) ;
1256+ if ( dropdown ) dropdown . remove ( ) ;
1257+ dropdown = document . createElement ( 'div' ) ;
1258+ dropdown . id = type + '-history-dropdown' ;
1259+ dropdown . style . position = 'absolute' ;
1260+ dropdown . style . zIndex = 2000 ;
1261+ dropdown . style . background = document . body . classList . contains ( 'dark-mode' ) ? '#23272a' : 'white' ;
1262+ dropdown . style . border = document . body . classList . contains ( 'dark-mode' ) ? '1px solid #444' : '1px solid #ccc' ;
1263+ dropdown . style . boxShadow = '0 2px 8px rgba(0,0,0,0.15)' ;
1264+ dropdown . style . fontSize = '13px' ;
1265+ dropdown . style . minWidth = '220px' ;
1266+ dropdown . style . maxHeight = '320px' ;
1267+ dropdown . style . overflowY = 'auto' ;
1268+ dropdown . style . cursor = 'pointer' ;
1269+ dropdown . style . padding = '4px 0' ;
1270+ dropdown . style . color = document . body . classList . contains ( 'dark-mode' ) ? '#e0e0e0' : '#222' ;
1271+ // Position below anchorBtn
1272+ const rect = anchorBtn . getBoundingClientRect ( ) ;
1273+ dropdown . style . left = rect . left + 'px' ;
1274+ dropdown . style . top = ( rect . bottom + window . scrollY ) + 'px' ;
1275+ items . forEach ( ( summary , idx ) => {
1276+ const item = document . createElement ( 'div' ) ;
1277+ item . textContent = ( stack . length - maxItems + idx + 1 ) + '. ' + summary ;
1278+ item . style . padding = '4px 12px' ;
1279+ item . style . borderBottom = document . body . classList . contains ( 'dark-mode' ) ? '1px solid #444' : '1px solid #eee' ;
1280+ item . onmouseenter = ( ) => { item . style . background = document . body . classList . contains ( 'dark-mode' ) ? '#2d3136' : '#f0f0f0' ; } ;
1281+ item . onmouseleave = ( ) => { item . style . background = document . body . classList . contains ( 'dark-mode' ) ? '#23272a' : 'white' ; } ;
1282+ item . onclick = ( ) => {
1283+ if ( type === 'undo' ) {
1284+ // Jump to selected undo state
1285+ const targetIdx = stack . length - maxItems + idx ;
1286+ if ( targetIdx >= 0 && targetIdx < stack . length ) {
1287+ // Move all newer states to redoStack
1288+ while ( undoStack . length > targetIdx + 1 ) {
1289+ redoStack . push ( undoStack . pop ( ) ) ;
1290+ }
1291+ editor . value = undoStack [ undoStack . length - 1 ] ;
1292+ updatePreview ( ) ;
1293+ updateStatusBar ( ) ;
1294+ lastUndoValue = editor . value ;
1295+ }
1296+ } else {
1297+ // Jump to selected redo state
1298+ const targetIdx = stack . length - maxItems + idx ;
1299+ if ( targetIdx >= 0 && targetIdx < stack . length ) {
1300+ // Move all older states to undoStack
1301+ while ( redoStack . length > targetIdx + 1 ) {
1302+ undoStack . push ( redoStack . pop ( ) ) ;
1303+ }
1304+ editor . value = redoStack [ redoStack . length - 1 ] ;
1305+ updatePreview ( ) ;
1306+ updateStatusBar ( ) ;
1307+ lastUndoValue = editor . value ;
1308+ }
1309+ }
1310+ dropdown . remove ( ) ;
1311+ } ;
1312+ dropdown . appendChild ( item ) ;
1313+ } ) ;
1314+ document . body . appendChild ( dropdown ) ;
1315+ // Keep dropdown visible while hovering the anchor button or the dropdown itself.
1316+ // Close when both are left, or on click outside. Small delay prevents flicker when moving between button and menu.
1317+ let closeTimeout = null ;
1318+ const scheduleClose = ( ) => {
1319+ if ( closeTimeout ) clearTimeout ( closeTimeout ) ;
1320+ closeTimeout = setTimeout ( ( ) => {
1321+ try {
1322+ if ( ! anchorBtn . matches ( ':hover' ) && ! dropdown . matches ( ':hover' ) ) {
1323+ if ( dropdown ) dropdown . remove ( ) ;
1324+ document . removeEventListener ( 'mousedown' , outsideHandler ) ;
1325+ }
1326+ } catch ( err ) { /* element may be gone */ }
1327+ } , 160 ) ;
1328+ } ;
1329+ const cancelClose = ( ) => { if ( closeTimeout ) { clearTimeout ( closeTimeout ) ; closeTimeout = null ; } } ;
1330+
1331+ const outsideHandler = ( e ) => {
1332+ if ( ! dropdown . contains ( e . target ) && e . target !== anchorBtn ) {
1333+ if ( dropdown ) dropdown . remove ( ) ;
1334+ document . removeEventListener ( 'mousedown' , outsideHandler ) ;
1335+ }
1336+ } ;
1337+
1338+ anchorBtn . addEventListener ( 'mouseleave' , scheduleClose ) ;
1339+ anchorBtn . addEventListener ( 'mouseenter' , cancelClose ) ;
1340+ dropdown . addEventListener ( 'mouseleave' , scheduleClose ) ;
1341+ dropdown . addEventListener ( 'mouseenter' , cancelClose ) ;
1342+ // Also close when clicking outside
1343+ setTimeout ( ( ) => { document . addEventListener ( 'mousedown' , outsideHandler ) ; } , 0 ) ;
1344+ }
1345+ // (removed duplicate header)
1346+ function saveToUndoStack ( e ) {
1347+ const value = editor . value ;
1348+ // If shift is held, always save whole text immediately
1349+ if ( e && e . shiftKey ) {
1350+ undoStack . push ( value ) ;
1351+ if ( undoStack . length > UNDO_STACK_LIMIT ) undoStack . shift ( ) ;
1352+ redoStack = [ ] ;
1353+ lastUndoValue = value ;
1354+ lastUndoSaveTime = Date . now ( ) ;
1355+ return ;
11741356 }
1175- redoStack = [ ] ;
1357+ // Throttle saves
1358+ if ( undoTimer ) clearTimeout ( undoTimer ) ;
1359+ undoTimer = setTimeout ( ( ) => {
1360+ // Detect incomplete word/typo: if last char is not space or punctuation, or if last word is not in dictionary
1361+ let incomplete = false ;
1362+ const lastWordMatch = value . match ( / ( \w + ) $ / ) ;
1363+ if ( lastWordMatch ) {
1364+ const lastWord = lastWordMatch [ 1 ] ;
1365+ // Simple typo detection: word length > 2 and not in basic dictionary
1366+ const basicDict = [ 'the' , 'and' , 'for' , 'with' , 'this' , 'that' , 'from' , 'have' , 'will' , 'can' , 'are' , 'was' , 'but' , 'not' , 'all' , 'any' , 'one' , 'two' , 'three' , 'four' , 'five' , 'six' , 'seven' , 'eight' , 'nine' , 'zero' , 'markdown' , 'editor' , 'bold' , 'italic' , 'code' , 'list' , 'table' , 'image' , 'link' , 'quote' , 'task' , 'footnote' , 'heading' , 'feature' , 'example' , 'function' , 'block' , 'write' , 'file' , 'open' , 'save' , 'undo' , 'redo' , 'preview' , 'split' , 'pane' , 'view' , 'help' , 'update' , 'changelog' , 'about' , 'menu' , 'toolbar' , 'button' , 'status' , 'line' , 'word' , 'character' , 'text' , 'content' , 'background' , 'color' , 'font' , 'size' , 'align' , 'left' , 'center' , 'right' , 'checkbox' , 'item' , 'definition' , 'hr' , 'horizontal' , 'rule' , 'subscript' , 'superscript' , 'insert' , 'select' , 'dialog' , 'popup' , 'alert' , 'find' , 'replace' , 'history' , 'search' , 'input' , 'output' , 'syntax' , 'highlight' , 'real' , 'time' , 'interface' , 'operations' , 'welcome' , 'happy' ] ;
1367+ if ( lastWord . length > 2 && ! basicDict . includes ( lastWord . toLowerCase ( ) ) ) {
1368+ incomplete = true ;
1369+ }
1370+ }
1371+ // Save if incomplete word/typo or value changed
1372+ if ( incomplete || value !== lastUndoValue ) {
1373+ undoStack . push ( value ) ;
1374+ if ( undoStack . length > UNDO_STACK_LIMIT ) undoStack . shift ( ) ;
1375+ redoStack = [ ] ;
1376+ lastUndoValue = value ;
1377+ lastUndoSaveTime = Date . now ( ) ;
1378+ }
1379+ } , undoThrottleMs ) ;
11761380 }
11771381
11781382 function undo ( ) {
@@ -1181,16 +1385,19 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
11811385 editor . value = undoStack [ undoStack . length - 1 ] || '' ;
11821386 updatePreview ( ) ;
11831387 updateStatusBar ( ) ;
1388+ lastUndoValue = editor . value ;
11841389 }
11851390 }
11861391
11871392 function redo ( ) {
11881393 if ( redoStack . length > 0 ) {
11891394 const redoValue = redoStack . pop ( ) ;
11901395 undoStack . push ( redoValue ) ;
1396+ if ( undoStack . length > UNDO_STACK_LIMIT ) undoStack . shift ( ) ;
11911397 editor . value = redoValue ;
11921398 updatePreview ( ) ;
11931399 updateStatusBar ( ) ;
1400+ lastUndoValue = redoValue ;
11941401 }
11951402 }
11961403
@@ -1485,6 +1692,13 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
14851692
14861693 // Menu functions (placeholder implementations)
14871694 function showAbout ( ) {
1695+ // Render local changelogs into the popup using existing markdown parser
1696+ const el = document . getElementById ( 'changelog-content' ) ;
1697+ try {
1698+ if ( el ) el . innerHTML = parseMarkdown ( LOCAL_CHANGELOG_MD ) ;
1699+ } catch ( err ) {
1700+ if ( el ) el . textContent = LOCAL_CHANGELOG_MD ;
1701+ }
14881702 document . getElementById ( 'changelog-popup' ) . style . display = 'block' ;
14891703 document . getElementById ( 'popup-overlay' ) . style . display = 'block' ;
14901704 }
@@ -1505,16 +1719,23 @@ <h3 id="dialog-title" style="margin-top:0;margin-bottom:15px;font-size:16px;">In
15051719 // Hide help popup
15061720 document . addEventListener ( 'DOMContentLoaded' , function ( ) {
15071721 var closeBtn = document . getElementById ( 'help-popup-close' ) ;
1508- if ( closeBtn ) closeBtn . onclick = function ( ) {
1509- document . getElementById ( 'help-popup' ) . style . display = 'none' ;
1510- document . getElementById ( 'popup-overlay' ) . style . display = 'none' ;
1511- } ;
1722+ if ( closeBtn ) closeBtn . onclick = function ( ) {
1723+ document . getElementById ( 'help-popup' ) . style . display = 'none' ;
1724+ document . getElementById ( 'popup-overlay' ) . style . display = 'none' ;
1725+ } ;
1726+ var changelogClose = document . getElementById ( 'changelog-popup-close' ) ;
1727+ if ( changelogClose ) changelogClose . onclick = function ( ) {
1728+ document . getElementById ( 'changelog-popup' ) . style . display = 'none' ;
1729+ document . getElementById ( 'popup-overlay' ) . style . display = 'none' ;
1730+ } ;
15121731 // Also close when clicking overlay
15131732 var overlay = document . getElementById ( 'popup-overlay' ) ;
1514- if ( overlay ) overlay . addEventListener ( 'click' , function ( ) {
1515- document . getElementById ( 'help-popup' ) . style . display = 'none' ;
1516- overlay . style . display = 'none' ;
1517- } ) ;
1733+ if ( overlay ) overlay . addEventListener ( 'click' , function ( ) {
1734+ // Close any open popups that use the overlay
1735+ var help = document . getElementById ( 'help-popup' ) ; if ( help ) help . style . display = 'none' ;
1736+ var changelog = document . getElementById ( 'changelog-popup' ) ; if ( changelog ) changelog . style . display = 'none' ;
1737+ overlay . style . display = 'none' ;
1738+ } ) ;
15181739 } ) ;
15191740
15201741 // Handle image info badge clicks
0 commit comments