@@ -33,7 +33,9 @@ document.addEventListener("DOMContentLoaded", function () {
3333 html2canvas : 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js' ,
3434 pako : 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js' ,
3535 joypixels : 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js' ,
36- joypixels_css : 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css'
36+ joypixels_css : 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css' ,
37+ filesaver : 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js' ,
38+ jsyaml : 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js'
3739 } ;
3840
3941 let markdownRenderTimeout = null ;
@@ -43,8 +45,24 @@ document.addEventListener("DOMContentLoaded", function () {
4345 let syncScrollingEnabled = true ;
4446 let isEditorScrolling = false ;
4547 let isPreviewScrolling = false ;
46- let scrollSyncTimeout = null ;
47- const SCROLL_SYNC_DELAY = 10 ;
48+ // Performance caching variables to prevent forced reflows / layout thrashing
49+ let cachedContainerLeft = 0 ;
50+ let cachedContainerWidth = 0 ;
51+ let cachedEditorPaneScrollHeight = 0 ;
52+ let cachedEditorPaneClientHeight = 0 ;
53+ let cachedPreviewPaneScrollHeight = 0 ;
54+ let cachedPreviewPaneClientHeight = 0 ;
55+
56+ function updateCachedPaneHeights ( ) {
57+ if ( editorPane ) {
58+ cachedEditorPaneScrollHeight = editorPane . scrollHeight ;
59+ cachedEditorPaneClientHeight = editorPane . clientHeight ;
60+ }
61+ if ( previewPane ) {
62+ cachedPreviewPaneScrollHeight = previewPane . scrollHeight ;
63+ cachedPreviewPaneClientHeight = previewPane . clientHeight ;
64+ }
65+ }
4866
4967 // View Mode State - Story 1.1
5068 let currentViewMode = 'split' ; // 'editor', 'split', or 'preview'
@@ -814,9 +832,26 @@ document.addEventListener("DOMContentLoaded", function () {
814832 } ) ;
815833 }
816834
835+ let isJsYamlLoading = false ;
817836 function parseFrontmatter ( markdown ) {
818837 const match = markdown . match ( / ^ - - - \r ? \n ( [ \s \S ] * ?) \r ? \n - - - ( \r ? \n | $ ) / ) ;
819838 if ( ! match ) return { frontmatter : null , body : markdown } ;
839+
840+ if ( typeof jsyaml === 'undefined' ) {
841+ if ( ! isJsYamlLoading ) {
842+ isJsYamlLoading = true ;
843+ loadScript ( CDN . jsyaml ) . then ( function ( ) {
844+ isJsYamlLoading = false ;
845+ _lastRenderedContent = null ;
846+ renderMarkdown ( ) ;
847+ } ) . catch ( function ( e ) {
848+ isJsYamlLoading = false ;
849+ console . warn ( 'Failed to load js-yaml:' , e ) ;
850+ } ) ;
851+ }
852+ return { frontmatter : null , body : markdown . slice ( match [ 0 ] . length ) } ;
853+ }
854+
820855 try {
821856 const data = jsyaml . load ( match [ 1 ] ) || { } ;
822857 return { frontmatter : data , body : markdown . slice ( match [ 0 ] . length ) } ;
@@ -1567,13 +1602,15 @@ document.addEventListener("DOMContentLoaded", function () {
15671602 container . classList . remove ( 'is-loading' ) ;
15681603 } ) ;
15691604 addMermaidToolbars ( ) ;
1605+ updateCachedPaneHeights ( ) ;
15701606 } )
15711607 . catch ( ( e ) => {
15721608 console . warn ( "Mermaid rendering failed:" , e ) ;
15731609 markdownPreview . querySelectorAll ( '.mermaid-container.is-loading' ) . forEach ( ( container ) => {
15741610 container . classList . remove ( 'is-loading' ) ;
15751611 } ) ;
15761612 addMermaidToolbars ( ) ;
1613+ updateCachedPaneHeights ( ) ;
15771614 } ) ;
15781615 } ;
15791616 if ( typeof mermaid === 'undefined' ) {
@@ -1599,6 +1636,7 @@ document.addEventListener("DOMContentLoaded", function () {
15991636 markdownPreview . querySelectorAll ( 'mjx-container[tabindex="0"]' ) . forEach ( function ( mjx ) {
16001637 mjx . removeAttribute ( 'tabindex' ) ;
16011638 } ) ;
1639+ updateCachedPaneHeights ( ) ;
16021640 } ) . catch ( function ( err ) {
16031641 console . warn ( 'MathJax typesetting failed:' , err ) ;
16041642 } ) ;
@@ -1625,6 +1663,7 @@ document.addEventListener("DOMContentLoaded", function () {
16251663 markdownPreview . querySelectorAll ( 'mjx-container[tabindex="0"]' ) . forEach ( function ( mjx ) {
16261664 mjx . removeAttribute ( 'tabindex' ) ;
16271665 } ) ;
1666+ updateCachedPaneHeights ( ) ;
16281667 } ) . catch ( function ( err ) {
16291668 console . warn ( 'MathJax typesetting failed:' , err ) ;
16301669 } ) ;
@@ -1639,6 +1678,7 @@ document.addEventListener("DOMContentLoaded", function () {
16391678 updateFindHighlights ( ) ;
16401679 cleanupImageObjectUrls ( ) ;
16411680 scheduleLineNumberUpdate ( ) ;
1681+ updateCachedPaneHeights ( ) ;
16421682 } catch ( e ) {
16431683 console . error ( "Markdown rendering failed:" , e ) ;
16441684 const safeMessage = escapeHtml ( e && e . message ? e . message : 'Unknown error' ) ;
@@ -2251,11 +2291,14 @@ document.addEventListener("DOMContentLoaded", function () {
22512291
22522292 if ( scrollSyncTimeout ) cancelAnimationFrame ( scrollSyncTimeout ) ;
22532293 scrollSyncTimeout = requestAnimationFrame ( function ( ) {
2294+ if ( cachedEditorPaneScrollHeight === 0 ) {
2295+ updateCachedPaneHeights ( ) ;
2296+ }
22542297 const editorScrollRatio =
22552298 editorPane . scrollTop /
2256- ( editorPane . scrollHeight - editorPane . clientHeight ) ;
2299+ ( cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight ) ;
22572300 const previewScrollPosition =
2258- ( previewPane . scrollHeight - previewPane . clientHeight ) *
2301+ ( cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight ) *
22592302 editorScrollRatio ;
22602303
22612304 if ( ! isNaN ( previewScrollPosition ) && isFinite ( previewScrollPosition ) ) {
@@ -2274,11 +2317,14 @@ document.addEventListener("DOMContentLoaded", function () {
22742317
22752318 if ( scrollSyncTimeout ) cancelAnimationFrame ( scrollSyncTimeout ) ;
22762319 scrollSyncTimeout = requestAnimationFrame ( function ( ) {
2320+ if ( cachedPreviewPaneScrollHeight === 0 ) {
2321+ updateCachedPaneHeights ( ) ;
2322+ }
22772323 const previewScrollRatio =
22782324 previewPane . scrollTop /
2279- ( previewPane . scrollHeight - previewPane . clientHeight ) ;
2325+ ( cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight ) ;
22802326 const editorScrollPosition =
2281- ( editorPane . scrollHeight - editorPane . clientHeight ) *
2327+ ( cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight ) *
22822328 previewScrollRatio ;
22832329
22842330 if ( ! isNaN ( editorScrollPosition ) && isFinite ( editorScrollPosition ) ) {
@@ -4915,6 +4961,13 @@ document.addEventListener("DOMContentLoaded", function () {
49154961 isResizing = true ;
49164962 resizeDivider . classList . add ( 'dragging' ) ;
49174963 document . body . classList . add ( 'resizing' ) ;
4964+
4965+ // Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag
4966+ if ( contentContainer ) {
4967+ const containerRect = contentContainer . getBoundingClientRect ( ) ;
4968+ cachedContainerLeft = containerRect . left ;
4969+ cachedContainerWidth = containerRect . width ;
4970+ }
49184971 }
49194972
49204973 function startResizeTouch ( e ) {
@@ -4924,17 +4977,22 @@ document.addEventListener("DOMContentLoaded", function () {
49244977 isResizing = true ;
49254978 resizeDivider . classList . add ( 'dragging' ) ;
49264979 document . body . classList . add ( 'resizing' ) ;
4980+
4981+ // Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag
4982+ if ( contentContainer ) {
4983+ const containerRect = contentContainer . getBoundingClientRect ( ) ;
4984+ cachedContainerLeft = containerRect . left ;
4985+ cachedContainerWidth = containerRect . width ;
4986+ }
49274987 }
49284988
49294989 function handleResize ( e ) {
49304990 if ( ! isResizing ) return ;
49314991
4932- const containerRect = contentContainer . getBoundingClientRect ( ) ;
4933- const containerWidth = containerRect . width ;
4934- const mouseX = e . clientX - containerRect . left ;
4992+ const mouseX = e . clientX - cachedContainerLeft ;
49354993
4936- // Calculate percentage
4937- let newEditorPercent = ( mouseX / containerWidth ) * 100 ;
4994+ // Calculate percentage using cached container width
4995+ let newEditorPercent = ( mouseX / cachedContainerWidth ) * 100 ;
49384996
49394997 // Enforce minimum pane widths
49404998 newEditorPercent = Math . max ( MIN_PANE_PERCENT , Math . min ( 100 - MIN_PANE_PERCENT , newEditorPercent ) ) ;
@@ -4946,11 +5004,9 @@ document.addEventListener("DOMContentLoaded", function () {
49465004 function handleResizeTouch ( e ) {
49475005 if ( ! isResizing || ! e . touches [ 0 ] ) return ;
49485006
4949- const containerRect = contentContainer . getBoundingClientRect ( ) ;
4950- const containerWidth = containerRect . width ;
4951- const touchX = e . touches [ 0 ] . clientX - containerRect . left ;
5007+ const touchX = e . touches [ 0 ] . clientX - cachedContainerLeft ;
49525008
4953- let newEditorPercent = ( touchX / containerWidth ) * 100 ;
5009+ let newEditorPercent = ( touchX / cachedContainerWidth ) * 100 ;
49545010 newEditorPercent = Math . max ( MIN_PANE_PERCENT , Math . min ( 100 - MIN_PANE_PERCENT , newEditorPercent ) ) ;
49555011
49565012 editorWidthPercent = newEditorPercent ;
@@ -4962,6 +5018,7 @@ document.addEventListener("DOMContentLoaded", function () {
49625018 isResizing = false ;
49635019 resizeDivider . classList . remove ( 'dragging' ) ;
49645020 document . body . classList . remove ( 'resizing' ) ;
5021+ updateCachedPaneHeights ( ) ;
49655022 }
49665023
49675024 function applyPaneWidths ( ) {
@@ -5121,6 +5178,7 @@ document.addEventListener("DOMContentLoaded", function () {
51215178 toggleFrDockMode ( true ) ;
51225179 }
51235180 constrainFloatingPanelPosition ( ) ;
5181+ updateCachedPaneHeights ( ) ;
51245182 } , 100 ) ;
51255183 } ) ;
51265184
@@ -5155,9 +5213,12 @@ document.addEventListener("DOMContentLoaded", function () {
51555213 scheduleLineNumberUpdate ( ) ;
51565214 } ) ;
51575215
5158- initMarkdownFormatToolbar ( ) ;
5159- initFindReplaceModal ( ) ;
5160- initAppModals ( ) ;
5216+ // Defer non-critical startup initializations to reduce startup time and TBT
5217+ setTimeout ( function ( ) {
5218+ initMarkdownFormatToolbar ( ) ;
5219+ initFindReplaceModal ( ) ;
5220+ initAppModals ( ) ;
5221+ } , 50 ) ;
51615222
51625223 // Editor key handlers for list continuation and indentation
51635224 markdownEditor . addEventListener ( "keydown" , function ( e ) {
@@ -5371,6 +5432,33 @@ document.addEventListener("DOMContentLoaded", function () {
53715432 this . value = "" ;
53725433 } ) ;
53735434
5435+ function triggerSaveAs ( blob , filename ) {
5436+ if ( typeof saveAs === 'undefined' ) {
5437+ const exportDropdownBtn = document . getElementById ( "exportDropdown" ) ;
5438+ const originalHtml = exportDropdownBtn ? exportDropdownBtn . innerHTML : null ;
5439+ if ( exportDropdownBtn ) {
5440+ exportDropdownBtn . innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> Loading...' ;
5441+ exportDropdownBtn . disabled = true ;
5442+ }
5443+ loadScript ( CDN . filesaver ) . then ( function ( ) {
5444+ if ( exportDropdownBtn ) {
5445+ exportDropdownBtn . innerHTML = originalHtml ;
5446+ exportDropdownBtn . disabled = false ;
5447+ }
5448+ saveAs ( blob , filename ) ;
5449+ } ) . catch ( function ( e ) {
5450+ if ( exportDropdownBtn ) {
5451+ exportDropdownBtn . innerHTML = originalHtml ;
5452+ exportDropdownBtn . disabled = false ;
5453+ }
5454+ console . error ( 'Failed to load FileSaver:' , e ) ;
5455+ alert ( 'Failed to load export library. Please check your internet connection.' ) ;
5456+ } ) ;
5457+ } else {
5458+ saveAs ( blob , filename ) ;
5459+ }
5460+ }
5461+
53745462 exportMd . addEventListener ( "click" , function ( ) {
53755463 if ( typeof Neutralino !== 'undefined' ) {
53765464 nativeSaveMarkdown ( ) ;
@@ -5380,7 +5468,7 @@ document.addEventListener("DOMContentLoaded", function () {
53805468 const blob = new Blob ( [ markdownEditor . value ] , {
53815469 type : "text/markdown;charset=utf-8" ,
53825470 } ) ;
5383- saveAs ( blob , "document.md" ) ;
5471+ triggerSaveAs ( blob , "document.md" ) ;
53845472 } catch ( e ) {
53855473 console . error ( "Export failed:" , e ) ;
53865474 alert ( "Export failed: " + e . message ) ;
@@ -5587,7 +5675,7 @@ document.addEventListener("DOMContentLoaded", function () {
55875675 if ( typeof Neutralino !== 'undefined' ) {
55885676 nativeSaveHtml ( fullHtml ) ;
55895677 } else {
5590- saveAs ( blob , "document.html" ) ;
5678+ triggerSaveAs ( blob , "document.html" ) ;
55915679 }
55925680 } catch ( e ) {
55935681 console . error ( "HTML export failed:" , e ) ;
0 commit comments