Skip to content

Commit 6dbb4c4

Browse files
Merge pull request #135 from ThisIs-Developer/perf-ux-a11y-optimizations
Performance, UX, and Accessibility Optimizations
2 parents f84b3fd + e94be4f commit 6dbb4c4

6 files changed

Lines changed: 230 additions & 116 deletions

File tree

desktop-app/resources/index.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
<!-- Updated libraries to latest versions with Subresource Integrity (SRI) -->
1919
<link rel="stylesheet" href="/libs/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
2020
<link rel="stylesheet" href="/libs/github-markdown.min.css" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous">
21-
<link rel="stylesheet" href="/libs/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
22-
<link rel="stylesheet" href="/libs/joypixels.min.css" integrity="sha384-4ok+tBQQdy5hcPT56tzcE11yQ2BkN0Py1uDE8ZOiXYstHOpUB61pJafm+NidByp4" crossorigin="anonymous">
21+
<link rel="preload" href="/libs/bootstrap-icons.min.css" as="style" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
22+
<noscript><link rel="stylesheet" href="/libs/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous"></noscript>
2323
<link rel="stylesheet" href="/styles.css">
2424

2525
<!-- Loading order optimized - ensure libraries are loaded asynchronously using defer -->
@@ -129,8 +129,8 @@ <h1 class="h4 mb-0 me-2">Markdown Viewer</h1>
129129

130130
<div id="mobile-menu-panel" class="mobile-menu-panel">
131131
<div class="mobile-menu-header">
132-
<h5>Menu</h5>
133-
<button id="close-mobile-menu" class="tool-button">
132+
<h2 class="h5 m-0">Menu</h2>
133+
<button id="close-mobile-menu" class="tool-button" aria-label="Close menu">
134134
<i class="bi bi-x-lg"></i>
135135
</button>
136136
</div>
@@ -510,7 +510,7 @@ <h3 class="modal-section-title">Keyboard shortcuts</h3>
510510
</div>
511511
<div class="modal-body">
512512
<div class="about-header">
513-
<img src="assets/icon.jpg" alt="Markdown Viewer logo" class="about-logo" />
513+
<img src="assets/icon.jpg" alt="Markdown Viewer logo" class="about-logo" width="64" height="64" />
514514
<div class="about-details">
515515
<h3 class="about-title">Markdown Viewer</h3>
516516
<p class="about-description">A GitHub-style Markdown editor with live preview, diagrams, math, syntax highlighting, and export tools.</p>
@@ -780,8 +780,8 @@ <h3 class="modal-section-title">Open-source credits</h3>
780780

781781
<main class="content-container">
782782
<div class="editor-pane is-loading">
783-
<div id="line-numbers" class="line-numbers" aria-hidden="true"></div>
784-
<div id="editor-highlight-layer" class="editor-highlight-layer" aria-hidden="true" tabindex="-1"></div>
783+
<div id="line-numbers" class="line-numbers" aria-hidden="true" inert></div>
784+
<div id="editor-highlight-layer" class="editor-highlight-layer" aria-hidden="true" tabindex="-1" inert></div>
785785
<div class="editor-skeleton" id="editor-skeleton" aria-hidden="true">
786786
<div class="skeleton-placeholder skeleton-title"></div>
787787
<div class="skeleton-placeholder skeleton-line skeleton-w90"></div>

desktop-app/resources/js/script.js

Lines changed: 107 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@ document.addEventListener("DOMContentLoaded", function () {
1212
});
1313
}
1414

15+
const _loadedStyles = new Set();
16+
function loadStyle(url) {
17+
if (_loadedStyles.has(url)) return Promise.resolve();
18+
return new Promise(function(resolve, reject) {
19+
const link = document.createElement('link');
20+
link.rel = 'stylesheet';
21+
link.href = url;
22+
link.onload = function() { _loadedStyles.add(url); resolve(); };
23+
link.onerror = function() { reject(new Error('Failed to load style: ' + url)); };
24+
document.head.appendChild(link);
25+
});
26+
}
27+
1528
// CDN URLs for lazy-loaded libraries
1629
const CDN = {
1730
mermaid: 'https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js',
1831
mathjax: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js',
1932
jspdf: 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
2033
html2canvas: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
2134
pako: 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js',
22-
joypixels: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js'
35+
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'
2337
};
2438

2539
let markdownRenderTimeout = null;
@@ -1129,19 +1143,27 @@ document.addEventListener("DOMContentLoaded", function () {
11291143
if (tabId) switchTab(tabId);
11301144
};
11311145

1132-
// "+ Create" button at end of tab list
1146+
// "+ Create" button at end of tab list (placed outside tabList to prevent ARIA child violation)
11331147
const newBtn = document.createElement('button');
11341148
newBtn.className = 'tab-new-btn';
11351149
newBtn.title = 'New Tab (Ctrl+T)';
11361150
newBtn.setAttribute('aria-label', 'Open new tab');
11371151
newBtn.innerHTML = '<i class="bi bi-plus-lg"></i>';
11381152
newBtn.addEventListener('click', function() { newTab(); });
1139-
tabList.appendChild(newBtn);
1153+
1154+
const resetBtn = document.getElementById('tab-reset-btn');
1155+
if (resetBtn) {
1156+
tabList.parentElement.insertBefore(newBtn, resetBtn);
1157+
} else {
1158+
tabList.parentElement.appendChild(newBtn);
1159+
}
11401160

1141-
// Auto-scroll active tab into view
1161+
// Auto-scroll active tab into view (paint-aligned to prevent forced reflows)
11421162
const activeItem = tabList.querySelector('.tab-item.active');
11431163
if (activeItem) {
1144-
activeItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
1164+
requestAnimationFrame(function() {
1165+
activeItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
1166+
});
11451167
}
11461168

11471169
// Arrow-key keyboard navigation inside tabList (WAI-ARIA compliance with manual selection)
@@ -1581,6 +1603,9 @@ document.addEventListener("DOMContentLoaded", function () {
15811603
// Configure and load MathJax on first use
15821604
window.MathJax = {
15831605
loader: { load: ['[tex]/ams', '[tex]/boldsymbol'] },
1606+
options: {
1607+
a11y: { inTabOrder: false }
1608+
},
15841609
tex: {
15851610
inlineMath: [['$', '$'], ['\\(', '\\)']],
15861611
displayMath: [['$$', '$$'], ['\\[', '\\]']],
@@ -2103,7 +2128,10 @@ document.addEventListener("DOMContentLoaded", function () {
21032128

21042129
// PERF-002: Lazy-load JoyPixels on first use
21052130
if (typeof joypixels === 'undefined') {
2106-
loadScript(CDN.joypixels).then(function() { processEmojis(element); });
2131+
Promise.all([
2132+
loadScript(CDN.joypixels),
2133+
loadStyle(CDN.joypixels_css)
2134+
]).then(function() { processEmojis(element); });
21072135
return;
21082136
}
21092137

@@ -3645,15 +3673,15 @@ document.addEventListener("DOMContentLoaded", function () {
36453673

36463674
function updateFindHighlights() {
36473675
if (!editorHighlightLayer) return;
3648-
const text = markdownEditor.value || '';
3649-
const scrollTop = markdownEditor.scrollTop;
3650-
const scrollLeft = markdownEditor.scrollLeft;
36513676
if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !findMatches.length) {
3652-
editorHighlightLayer.textContent = text;
3653-
editorHighlightLayer.scrollTop = scrollTop;
3654-
editorHighlightLayer.scrollLeft = scrollLeft;
3677+
if (editorHighlightLayer.textContent !== '') {
3678+
editorHighlightLayer.textContent = '';
3679+
}
36553680
return;
36563681
}
3682+
const text = markdownEditor.value || '';
3683+
const scrollTop = markdownEditor.scrollTop;
3684+
const scrollLeft = markdownEditor.scrollLeft;
36573685
const fragment = document.createDocumentFragment();
36583686
let lastIndex = 0;
36593687
findMatches.forEach(function(match, index) {
@@ -3684,65 +3712,95 @@ document.addEventListener("DOMContentLoaded", function () {
36843712
editorPaneElement.style.setProperty('--line-number-gutter', gutterSize);
36853713
}
36863714

3687-
function ensureLineNumberMeasure() {
3688-
if (!lineNumbers) return;
3689-
if (!lineNumberMeasure) {
3690-
lineNumberMeasure = document.createElement('div');
3691-
lineNumberMeasure.setAttribute('aria-hidden', 'true');
3692-
document.body.appendChild(lineNumberMeasure);
3693-
}
3694-
const styles = window.getComputedStyle(markdownEditor);
3695-
lineNumberMeasure.style.position = 'absolute';
3696-
lineNumberMeasure.style.visibility = 'hidden';
3697-
lineNumberMeasure.style.whiteSpace = 'pre-wrap';
3698-
lineNumberMeasure.style.wordWrap = 'break-word';
3699-
lineNumberMeasure.style.boxSizing = 'border-box';
3700-
lineNumberMeasure.style.padding = styles.padding;
3701-
lineNumberMeasure.style.fontFamily = styles.fontFamily;
3702-
lineNumberMeasure.style.fontSize = styles.fontSize;
3703-
lineNumberMeasure.style.lineHeight = styles.lineHeight;
3704-
lineNumberMeasure.style.letterSpacing = styles.letterSpacing;
3705-
lineNumberMeasure.style.width = `${markdownEditor.clientWidth}px`;
3706-
lineNumberMeasure.style.top = '-9999px';
3707-
lineNumberMeasure.style.left = '-9999px';
3708-
}
3709-
37103715
function getLineHeight(styles) {
37113716
const computed = parseFloat(styles.lineHeight);
37123717
if (!Number.isNaN(computed)) return computed;
37133718
const fontSize = parseFloat(styles.fontSize) || 14;
37143719
return fontSize * 1.5;
37153720
}
37163721

3717-
function getWrappedLineCount(line, lineHeight, paddingSum) {
3718-
if (!lineNumberMeasure) return 1;
3719-
lineNumberMeasure.textContent = line.length ? line : LINE_NUMBER_EMPTY_PLACEHOLDER;
3720-
const contentHeight = lineNumberMeasure.scrollHeight - paddingSum;
3721-
return Math.max(1, Math.round(contentHeight / lineHeight));
3722+
function getWrappedLineCountMonospace(lineText, maxCharsPerLine) {
3723+
if (!lineText) return 1;
3724+
const words = lineText.replace(/\t/g, ' ').split(' ');
3725+
let linesCount = 1;
3726+
let currentLineLength = 0;
3727+
3728+
for (let i = 0; i < words.length; i++) {
3729+
const word = words[i];
3730+
const wordLength = word.length;
3731+
3732+
if (wordLength === 0) {
3733+
if (currentLineLength + 1 > maxCharsPerLine) {
3734+
linesCount++;
3735+
currentLineLength = 1;
3736+
} else {
3737+
currentLineLength++;
3738+
}
3739+
continue;
3740+
}
3741+
3742+
if (wordLength > maxCharsPerLine) {
3743+
const remainingSpace = maxCharsPerLine - currentLineLength;
3744+
if (remainingSpace > 0 && currentLineLength > 0) {
3745+
const firstPart = wordLength - remainingSpace;
3746+
linesCount += 1 + Math.floor(firstPart / maxCharsPerLine);
3747+
currentLineLength = firstPart % maxCharsPerLine;
3748+
} else {
3749+
linesCount += Math.floor(wordLength / maxCharsPerLine);
3750+
currentLineLength = wordLength % maxCharsPerLine;
3751+
}
3752+
continue;
3753+
}
3754+
3755+
const spaceRequired = currentLineLength === 0 ? 0 : 1;
3756+
if (currentLineLength + spaceRequired + wordLength > maxCharsPerLine) {
3757+
linesCount++;
3758+
currentLineLength = wordLength;
3759+
} else {
3760+
currentLineLength += spaceRequired + wordLength;
3761+
}
3762+
}
3763+
3764+
return Math.max(1, linesCount);
37223765
}
37233766

37243767
const lineCache = new Map();
37253768
let lastEditorWidth = 0;
3769+
let charWidth = 0;
3770+
let maxCharsPerLine = 0;
37263771

37273772
function updateLineNumbers() {
37283773
if (!lineNumbers || !markdownEditor) return;
37293774
const lines = (markdownEditor.value || '').split('\n');
37303775
const lineCount = Math.max(1, lines.length);
37313776

3732-
// Clear height cache if editor width has changed
37333777
const currentWidth = markdownEditor.clientWidth;
3734-
if (currentWidth !== lastEditorWidth) {
3778+
const styles = window.getComputedStyle(markdownEditor);
3779+
const paddingLeft = parseFloat(styles.paddingLeft) || 10;
3780+
const paddingRight = parseFloat(styles.paddingRight) || 10;
3781+
const availableWidth = currentWidth - paddingLeft - paddingRight;
3782+
3783+
// Measure character width exactly once per resize / layout width change
3784+
if (currentWidth !== lastEditorWidth || charWidth === 0) {
37353785
lineCache.clear();
37363786
lastEditorWidth = currentWidth;
3787+
3788+
const testSpan = document.createElement('span');
3789+
testSpan.style.fontFamily = styles.fontFamily;
3790+
testSpan.style.fontSize = styles.fontSize;
3791+
testSpan.style.visibility = 'hidden';
3792+
testSpan.style.position = 'absolute';
3793+
testSpan.style.whiteSpace = 'pre';
3794+
testSpan.textContent = 'a'.repeat(100);
3795+
document.body.appendChild(testSpan);
3796+
charWidth = testSpan.getBoundingClientRect().width / 100;
3797+
document.body.removeChild(testSpan);
3798+
3799+
maxCharsPerLine = Math.max(1, Math.floor(availableWidth / charWidth));
37373800
}
37383801

37393802
updateLineNumberGutter(lineCount);
3740-
ensureLineNumberMeasure();
3741-
const styles = window.getComputedStyle(markdownEditor);
37423803
const lineHeight = getLineHeight(styles);
3743-
const paddingSum =
3744-
(parseFloat(styles.paddingTop) || 0) +
3745-
(parseFloat(styles.paddingBottom) || 0);
37463804

37473805
const existingItems = lineNumbers.children;
37483806
const existingCount = existingItems.length;
@@ -3762,12 +3820,12 @@ document.addEventListener("DOMContentLoaded", function () {
37623820
}
37633821
}
37643822

3765-
// Update only the heights and numbers that changed, querying cache
3823+
// Update only the heights and numbers that changed, using monospace simulator to avoid forced reflows
37663824
for (let i = 0; i < lineCount; i += 1) {
37673825
const lineText = lines[i];
37683826
let wrapHeight = lineCache.get(lineText);
37693827
if (wrapHeight === undefined) {
3770-
const wrapCount = getWrappedLineCount(lineText, lineHeight, paddingSum);
3828+
const wrapCount = getWrappedLineCountMonospace(lineText, maxCharsPerLine);
37713829
wrapHeight = wrapCount * lineHeight;
37723830
lineCache.set(lineText, wrapHeight);
37733831
}

desktop-app/resources/styles.css

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,7 @@ body {
555555
font-size: 14px;
556556
line-height: 1.5;
557557
text-align: right;
558-
color: var(--text-color);
559-
opacity: 0.55;
558+
color: var(--text-secondary);
560559
background-color: var(--editor-bg);
561560
border-right: 1px solid var(--border-color);
562561
box-sizing: border-box;

index.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@
8080
<!-- Updated libraries to latest versions with Subresource Integrity (SRI) -->
8181
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
8282
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous">
83-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
84-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css" integrity="sha384-4ok+tBQQdy5hcPT56tzcE11yQ2BkN0Py1uDE8ZOiXYstHOpUB61pJafm+NidByp4" crossorigin="anonymous">
83+
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" as="style" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
84+
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous"></noscript>
8585
<link rel="stylesheet" href="styles.css">
8686

8787
<!-- Loading order optimized - ensure libraries are loaded asynchronously using defer -->
@@ -188,8 +188,8 @@ <h1 class="h4 mb-0 me-2">Markdown Viewer</h1>
188188

189189
<div id="mobile-menu-panel" class="mobile-menu-panel">
190190
<div class="mobile-menu-header">
191-
<h5>Menu</h5>
192-
<button id="close-mobile-menu" class="tool-button">
191+
<h2 class="h5 m-0">Menu</h2>
192+
<button id="close-mobile-menu" class="tool-button" aria-label="Close menu">
193193
<i class="bi bi-x-lg"></i>
194194
</button>
195195
</div>
@@ -569,7 +569,7 @@ <h3 class="modal-section-title">Keyboard shortcuts</h3>
569569
</div>
570570
<div class="modal-body">
571571
<div class="about-header">
572-
<img src="assets/icon.jpg" alt="Markdown Viewer logo" class="about-logo" />
572+
<img src="assets/icon.jpg" alt="Markdown Viewer logo" class="about-logo" width="64" height="64" />
573573
<div class="about-details">
574574
<h3 class="about-title">Markdown Viewer</h3>
575575
<p class="about-description">A GitHub-style Markdown editor with live preview, diagrams, math, syntax highlighting, and export tools.</p>
@@ -839,8 +839,8 @@ <h3 class="modal-section-title">Open-source credits</h3>
839839

840840
<main class="content-container">
841841
<div class="editor-pane is-loading">
842-
<div id="line-numbers" class="line-numbers" aria-hidden="true"></div>
843-
<div id="editor-highlight-layer" class="editor-highlight-layer" aria-hidden="true" tabindex="-1"></div>
842+
<div id="line-numbers" class="line-numbers" aria-hidden="true" inert></div>
843+
<div id="editor-highlight-layer" class="editor-highlight-layer" aria-hidden="true" tabindex="-1" inert></div>
844844
<div class="editor-skeleton" id="editor-skeleton" aria-hidden="true">
845845
<div class="skeleton-placeholder skeleton-title"></div>
846846
<div class="skeleton-placeholder skeleton-line skeleton-w90"></div>

0 commit comments

Comments
 (0)