diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index b02882e..3af9020 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -15,20 +15,89 @@ Markdown Viewer - - - + + + + + + + + + + + + - - - - + + + + + + - - -
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 4a986d2..81dfe7b 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -33,7 +33,9 @@ document.addEventListener("DOMContentLoaded", function () { html2canvas: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', pako: 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js', joypixels: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js', - joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css' + joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css', + filesaver: 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js', + jsyaml: 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js' }; let markdownRenderTimeout = null; @@ -43,8 +45,24 @@ document.addEventListener("DOMContentLoaded", function () { let syncScrollingEnabled = true; let isEditorScrolling = false; let isPreviewScrolling = false; - let scrollSyncTimeout = null; - const SCROLL_SYNC_DELAY = 10; + // Performance caching variables to prevent forced reflows / layout thrashing + let cachedContainerLeft = 0; + let cachedContainerWidth = 0; + let cachedEditorPaneScrollHeight = 0; + let cachedEditorPaneClientHeight = 0; + let cachedPreviewPaneScrollHeight = 0; + let cachedPreviewPaneClientHeight = 0; + + function updateCachedPaneHeights() { + if (editorPane) { + cachedEditorPaneScrollHeight = editorPane.scrollHeight; + cachedEditorPaneClientHeight = editorPane.clientHeight; + } + if (previewPane) { + cachedPreviewPaneScrollHeight = previewPane.scrollHeight; + cachedPreviewPaneClientHeight = previewPane.clientHeight; + } + } // View Mode State - Story 1.1 let currentViewMode = 'split'; // 'editor', 'split', or 'preview' @@ -814,9 +832,26 @@ document.addEventListener("DOMContentLoaded", function () { }); } + let isJsYamlLoading = false; function parseFrontmatter(markdown) { const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/); if (!match) return { frontmatter: null, body: markdown }; + + if (typeof jsyaml === 'undefined') { + if (!isJsYamlLoading) { + isJsYamlLoading = true; + loadScript(CDN.jsyaml).then(function() { + isJsYamlLoading = false; + _lastRenderedContent = null; + renderMarkdown(); + }).catch(function(e) { + isJsYamlLoading = false; + console.warn('Failed to load js-yaml:', e); + }); + } + return { frontmatter: null, body: markdown.slice(match[0].length) }; + } + try { const data = jsyaml.load(match[1]) || {}; return { frontmatter: data, body: markdown.slice(match[0].length) }; @@ -1567,6 +1602,7 @@ document.addEventListener("DOMContentLoaded", function () { container.classList.remove('is-loading'); }); addMermaidToolbars(); + updateCachedPaneHeights(); }) .catch((e) => { console.warn("Mermaid rendering failed:", e); @@ -1574,6 +1610,7 @@ document.addEventListener("DOMContentLoaded", function () { container.classList.remove('is-loading'); }); addMermaidToolbars(); + updateCachedPaneHeights(); }); }; if (typeof mermaid === 'undefined') { @@ -1599,6 +1636,7 @@ document.addEventListener("DOMContentLoaded", function () { markdownPreview.querySelectorAll('mjx-container[tabindex="0"]').forEach(function(mjx) { mjx.removeAttribute('tabindex'); }); + updateCachedPaneHeights(); }).catch(function(err) { console.warn('MathJax typesetting failed:', err); }); @@ -1625,6 +1663,7 @@ document.addEventListener("DOMContentLoaded", function () { markdownPreview.querySelectorAll('mjx-container[tabindex="0"]').forEach(function(mjx) { mjx.removeAttribute('tabindex'); }); + updateCachedPaneHeights(); }).catch(function(err) { console.warn('MathJax typesetting failed:', err); }); @@ -1639,6 +1678,7 @@ document.addEventListener("DOMContentLoaded", function () { updateFindHighlights(); cleanupImageObjectUrls(); scheduleLineNumberUpdate(); + updateCachedPaneHeights(); } catch (e) { console.error("Markdown rendering failed:", e); const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error'); @@ -2251,11 +2291,14 @@ document.addEventListener("DOMContentLoaded", function () { if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout); scrollSyncTimeout = requestAnimationFrame(function() { + if (cachedEditorPaneScrollHeight === 0) { + updateCachedPaneHeights(); + } const editorScrollRatio = editorPane.scrollTop / - (editorPane.scrollHeight - editorPane.clientHeight); + (cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight); const previewScrollPosition = - (previewPane.scrollHeight - previewPane.clientHeight) * + (cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight) * editorScrollRatio; if (!isNaN(previewScrollPosition) && isFinite(previewScrollPosition)) { @@ -2274,11 +2317,14 @@ document.addEventListener("DOMContentLoaded", function () { if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout); scrollSyncTimeout = requestAnimationFrame(function() { + if (cachedPreviewPaneScrollHeight === 0) { + updateCachedPaneHeights(); + } const previewScrollRatio = previewPane.scrollTop / - (previewPane.scrollHeight - previewPane.clientHeight); + (cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight); const editorScrollPosition = - (editorPane.scrollHeight - editorPane.clientHeight) * + (cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight) * previewScrollRatio; if (!isNaN(editorScrollPosition) && isFinite(editorScrollPosition)) { @@ -4915,6 +4961,13 @@ document.addEventListener("DOMContentLoaded", function () { isResizing = true; resizeDivider.classList.add('dragging'); document.body.classList.add('resizing'); + + // Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag + if (contentContainer) { + const containerRect = contentContainer.getBoundingClientRect(); + cachedContainerLeft = containerRect.left; + cachedContainerWidth = containerRect.width; + } } function startResizeTouch(e) { @@ -4924,17 +4977,22 @@ document.addEventListener("DOMContentLoaded", function () { isResizing = true; resizeDivider.classList.add('dragging'); document.body.classList.add('resizing'); + + // Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag + if (contentContainer) { + const containerRect = contentContainer.getBoundingClientRect(); + cachedContainerLeft = containerRect.left; + cachedContainerWidth = containerRect.width; + } } function handleResize(e) { if (!isResizing) return; - const containerRect = contentContainer.getBoundingClientRect(); - const containerWidth = containerRect.width; - const mouseX = e.clientX - containerRect.left; + const mouseX = e.clientX - cachedContainerLeft; - // Calculate percentage - let newEditorPercent = (mouseX / containerWidth) * 100; + // Calculate percentage using cached container width + let newEditorPercent = (mouseX / cachedContainerWidth) * 100; // Enforce minimum pane widths newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent)); @@ -4946,11 +5004,9 @@ document.addEventListener("DOMContentLoaded", function () { function handleResizeTouch(e) { if (!isResizing || !e.touches[0]) return; - const containerRect = contentContainer.getBoundingClientRect(); - const containerWidth = containerRect.width; - const touchX = e.touches[0].clientX - containerRect.left; + const touchX = e.touches[0].clientX - cachedContainerLeft; - let newEditorPercent = (touchX / containerWidth) * 100; + let newEditorPercent = (touchX / cachedContainerWidth) * 100; newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent)); editorWidthPercent = newEditorPercent; @@ -4962,6 +5018,7 @@ document.addEventListener("DOMContentLoaded", function () { isResizing = false; resizeDivider.classList.remove('dragging'); document.body.classList.remove('resizing'); + updateCachedPaneHeights(); } function applyPaneWidths() { @@ -5121,6 +5178,7 @@ document.addEventListener("DOMContentLoaded", function () { toggleFrDockMode(true); } constrainFloatingPanelPosition(); + updateCachedPaneHeights(); }, 100); }); @@ -5155,9 +5213,12 @@ document.addEventListener("DOMContentLoaded", function () { scheduleLineNumberUpdate(); }); - initMarkdownFormatToolbar(); - initFindReplaceModal(); - initAppModals(); + // Defer non-critical startup initializations to reduce startup time and TBT + setTimeout(function() { + initMarkdownFormatToolbar(); + initFindReplaceModal(); + initAppModals(); + }, 50); // Editor key handlers for list continuation and indentation markdownEditor.addEventListener("keydown", function(e) { @@ -5371,6 +5432,33 @@ document.addEventListener("DOMContentLoaded", function () { this.value = ""; }); + function triggerSaveAs(blob, filename) { + if (typeof saveAs === 'undefined') { + const exportDropdownBtn = document.getElementById("exportDropdown"); + const originalHtml = exportDropdownBtn ? exportDropdownBtn.innerHTML : null; + if (exportDropdownBtn) { + exportDropdownBtn.innerHTML = ' Loading...'; + exportDropdownBtn.disabled = true; + } + loadScript(CDN.filesaver).then(function() { + if (exportDropdownBtn) { + exportDropdownBtn.innerHTML = originalHtml; + exportDropdownBtn.disabled = false; + } + saveAs(blob, filename); + }).catch(function(e) { + if (exportDropdownBtn) { + exportDropdownBtn.innerHTML = originalHtml; + exportDropdownBtn.disabled = false; + } + console.error('Failed to load FileSaver:', e); + alert('Failed to load export library. Please check your internet connection.'); + }); + } else { + saveAs(blob, filename); + } + } + exportMd.addEventListener("click", function () { if (typeof Neutralino !== 'undefined') { nativeSaveMarkdown(); @@ -5380,7 +5468,7 @@ document.addEventListener("DOMContentLoaded", function () { const blob = new Blob([markdownEditor.value], { type: "text/markdown;charset=utf-8", }); - saveAs(blob, "document.md"); + triggerSaveAs(blob, "document.md"); } catch (e) { console.error("Export failed:", e); alert("Export failed: " + e.message); @@ -5587,7 +5675,7 @@ document.addEventListener("DOMContentLoaded", function () { if (typeof Neutralino !== 'undefined') { nativeSaveHtml(fullHtml); } else { - saveAs(blob, "document.html"); + triggerSaveAs(blob, "document.html"); } } catch (e) { console.error("HTML export failed:", e); diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index fb88af6..b73f74c 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -1,3 +1,10 @@ +@font-face { + font-family: "bootstrap-icons"; + src: url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2?dd670da4167998394e1cf6f26487e45e") format("woff2"), + url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff?dd670da4167998394e1cf6f26487e45e") format("woff"); + font-display: block; +} + :root { --bg-color: #ffffff; --editor-bg: #f6f8fa; diff --git a/index.html b/index.html index f68c1b7..09e8923 100644 --- a/index.html +++ b/index.html @@ -77,20 +77,89 @@ Markdown Viewer - - - + + + + + + + + + + + + - - - - + + + + + + - - -
diff --git a/script.js b/script.js index 4a986d2..81dfe7b 100644 --- a/script.js +++ b/script.js @@ -33,7 +33,9 @@ document.addEventListener("DOMContentLoaded", function () { html2canvas: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', pako: 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js', joypixels: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js', - joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css' + joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css', + filesaver: 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js', + jsyaml: 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js' }; let markdownRenderTimeout = null; @@ -43,8 +45,24 @@ document.addEventListener("DOMContentLoaded", function () { let syncScrollingEnabled = true; let isEditorScrolling = false; let isPreviewScrolling = false; - let scrollSyncTimeout = null; - const SCROLL_SYNC_DELAY = 10; + // Performance caching variables to prevent forced reflows / layout thrashing + let cachedContainerLeft = 0; + let cachedContainerWidth = 0; + let cachedEditorPaneScrollHeight = 0; + let cachedEditorPaneClientHeight = 0; + let cachedPreviewPaneScrollHeight = 0; + let cachedPreviewPaneClientHeight = 0; + + function updateCachedPaneHeights() { + if (editorPane) { + cachedEditorPaneScrollHeight = editorPane.scrollHeight; + cachedEditorPaneClientHeight = editorPane.clientHeight; + } + if (previewPane) { + cachedPreviewPaneScrollHeight = previewPane.scrollHeight; + cachedPreviewPaneClientHeight = previewPane.clientHeight; + } + } // View Mode State - Story 1.1 let currentViewMode = 'split'; // 'editor', 'split', or 'preview' @@ -814,9 +832,26 @@ document.addEventListener("DOMContentLoaded", function () { }); } + let isJsYamlLoading = false; function parseFrontmatter(markdown) { const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/); if (!match) return { frontmatter: null, body: markdown }; + + if (typeof jsyaml === 'undefined') { + if (!isJsYamlLoading) { + isJsYamlLoading = true; + loadScript(CDN.jsyaml).then(function() { + isJsYamlLoading = false; + _lastRenderedContent = null; + renderMarkdown(); + }).catch(function(e) { + isJsYamlLoading = false; + console.warn('Failed to load js-yaml:', e); + }); + } + return { frontmatter: null, body: markdown.slice(match[0].length) }; + } + try { const data = jsyaml.load(match[1]) || {}; return { frontmatter: data, body: markdown.slice(match[0].length) }; @@ -1567,6 +1602,7 @@ document.addEventListener("DOMContentLoaded", function () { container.classList.remove('is-loading'); }); addMermaidToolbars(); + updateCachedPaneHeights(); }) .catch((e) => { console.warn("Mermaid rendering failed:", e); @@ -1574,6 +1610,7 @@ document.addEventListener("DOMContentLoaded", function () { container.classList.remove('is-loading'); }); addMermaidToolbars(); + updateCachedPaneHeights(); }); }; if (typeof mermaid === 'undefined') { @@ -1599,6 +1636,7 @@ document.addEventListener("DOMContentLoaded", function () { markdownPreview.querySelectorAll('mjx-container[tabindex="0"]').forEach(function(mjx) { mjx.removeAttribute('tabindex'); }); + updateCachedPaneHeights(); }).catch(function(err) { console.warn('MathJax typesetting failed:', err); }); @@ -1625,6 +1663,7 @@ document.addEventListener("DOMContentLoaded", function () { markdownPreview.querySelectorAll('mjx-container[tabindex="0"]').forEach(function(mjx) { mjx.removeAttribute('tabindex'); }); + updateCachedPaneHeights(); }).catch(function(err) { console.warn('MathJax typesetting failed:', err); }); @@ -1639,6 +1678,7 @@ document.addEventListener("DOMContentLoaded", function () { updateFindHighlights(); cleanupImageObjectUrls(); scheduleLineNumberUpdate(); + updateCachedPaneHeights(); } catch (e) { console.error("Markdown rendering failed:", e); const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error'); @@ -2251,11 +2291,14 @@ document.addEventListener("DOMContentLoaded", function () { if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout); scrollSyncTimeout = requestAnimationFrame(function() { + if (cachedEditorPaneScrollHeight === 0) { + updateCachedPaneHeights(); + } const editorScrollRatio = editorPane.scrollTop / - (editorPane.scrollHeight - editorPane.clientHeight); + (cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight); const previewScrollPosition = - (previewPane.scrollHeight - previewPane.clientHeight) * + (cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight) * editorScrollRatio; if (!isNaN(previewScrollPosition) && isFinite(previewScrollPosition)) { @@ -2274,11 +2317,14 @@ document.addEventListener("DOMContentLoaded", function () { if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout); scrollSyncTimeout = requestAnimationFrame(function() { + if (cachedPreviewPaneScrollHeight === 0) { + updateCachedPaneHeights(); + } const previewScrollRatio = previewPane.scrollTop / - (previewPane.scrollHeight - previewPane.clientHeight); + (cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight); const editorScrollPosition = - (editorPane.scrollHeight - editorPane.clientHeight) * + (cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight) * previewScrollRatio; if (!isNaN(editorScrollPosition) && isFinite(editorScrollPosition)) { @@ -4915,6 +4961,13 @@ document.addEventListener("DOMContentLoaded", function () { isResizing = true; resizeDivider.classList.add('dragging'); document.body.classList.add('resizing'); + + // Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag + if (contentContainer) { + const containerRect = contentContainer.getBoundingClientRect(); + cachedContainerLeft = containerRect.left; + cachedContainerWidth = containerRect.width; + } } function startResizeTouch(e) { @@ -4924,17 +4977,22 @@ document.addEventListener("DOMContentLoaded", function () { isResizing = true; resizeDivider.classList.add('dragging'); document.body.classList.add('resizing'); + + // Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag + if (contentContainer) { + const containerRect = contentContainer.getBoundingClientRect(); + cachedContainerLeft = containerRect.left; + cachedContainerWidth = containerRect.width; + } } function handleResize(e) { if (!isResizing) return; - const containerRect = contentContainer.getBoundingClientRect(); - const containerWidth = containerRect.width; - const mouseX = e.clientX - containerRect.left; + const mouseX = e.clientX - cachedContainerLeft; - // Calculate percentage - let newEditorPercent = (mouseX / containerWidth) * 100; + // Calculate percentage using cached container width + let newEditorPercent = (mouseX / cachedContainerWidth) * 100; // Enforce minimum pane widths newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent)); @@ -4946,11 +5004,9 @@ document.addEventListener("DOMContentLoaded", function () { function handleResizeTouch(e) { if (!isResizing || !e.touches[0]) return; - const containerRect = contentContainer.getBoundingClientRect(); - const containerWidth = containerRect.width; - const touchX = e.touches[0].clientX - containerRect.left; + const touchX = e.touches[0].clientX - cachedContainerLeft; - let newEditorPercent = (touchX / containerWidth) * 100; + let newEditorPercent = (touchX / cachedContainerWidth) * 100; newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent)); editorWidthPercent = newEditorPercent; @@ -4962,6 +5018,7 @@ document.addEventListener("DOMContentLoaded", function () { isResizing = false; resizeDivider.classList.remove('dragging'); document.body.classList.remove('resizing'); + updateCachedPaneHeights(); } function applyPaneWidths() { @@ -5121,6 +5178,7 @@ document.addEventListener("DOMContentLoaded", function () { toggleFrDockMode(true); } constrainFloatingPanelPosition(); + updateCachedPaneHeights(); }, 100); }); @@ -5155,9 +5213,12 @@ document.addEventListener("DOMContentLoaded", function () { scheduleLineNumberUpdate(); }); - initMarkdownFormatToolbar(); - initFindReplaceModal(); - initAppModals(); + // Defer non-critical startup initializations to reduce startup time and TBT + setTimeout(function() { + initMarkdownFormatToolbar(); + initFindReplaceModal(); + initAppModals(); + }, 50); // Editor key handlers for list continuation and indentation markdownEditor.addEventListener("keydown", function(e) { @@ -5371,6 +5432,33 @@ document.addEventListener("DOMContentLoaded", function () { this.value = ""; }); + function triggerSaveAs(blob, filename) { + if (typeof saveAs === 'undefined') { + const exportDropdownBtn = document.getElementById("exportDropdown"); + const originalHtml = exportDropdownBtn ? exportDropdownBtn.innerHTML : null; + if (exportDropdownBtn) { + exportDropdownBtn.innerHTML = ' Loading...'; + exportDropdownBtn.disabled = true; + } + loadScript(CDN.filesaver).then(function() { + if (exportDropdownBtn) { + exportDropdownBtn.innerHTML = originalHtml; + exportDropdownBtn.disabled = false; + } + saveAs(blob, filename); + }).catch(function(e) { + if (exportDropdownBtn) { + exportDropdownBtn.innerHTML = originalHtml; + exportDropdownBtn.disabled = false; + } + console.error('Failed to load FileSaver:', e); + alert('Failed to load export library. Please check your internet connection.'); + }); + } else { + saveAs(blob, filename); + } + } + exportMd.addEventListener("click", function () { if (typeof Neutralino !== 'undefined') { nativeSaveMarkdown(); @@ -5380,7 +5468,7 @@ document.addEventListener("DOMContentLoaded", function () { const blob = new Blob([markdownEditor.value], { type: "text/markdown;charset=utf-8", }); - saveAs(blob, "document.md"); + triggerSaveAs(blob, "document.md"); } catch (e) { console.error("Export failed:", e); alert("Export failed: " + e.message); @@ -5587,7 +5675,7 @@ document.addEventListener("DOMContentLoaded", function () { if (typeof Neutralino !== 'undefined') { nativeSaveHtml(fullHtml); } else { - saveAs(blob, "document.html"); + triggerSaveAs(blob, "document.html"); } } catch (e) { console.error("HTML export failed:", e); diff --git a/styles.css b/styles.css index fb88af6..b73f74c 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,10 @@ +@font-face { + font-family: "bootstrap-icons"; + src: url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2?dd670da4167998394e1cf6f26487e45e") format("woff2"), + url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff?dd670da4167998394e1cf6f26487e45e") format("woff"); + font-display: block; +} + :root { --bg-color: #ffffff; --editor-bg: #f6f8fa;