Skip to content

Commit 841a184

Browse files
Merge pull request #170 from ThisIs-Developer/fix/toc-scrolling-169
fix: resolve Table of Contents smooth scroll navigation (#169)
2 parents 03c7bdb + fc5e16f commit 841a184

5 files changed

Lines changed: 347 additions & 7 deletions

File tree

desktop-app/resources/js/preview-worker.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,20 @@ function configureMarked() {
319319
return `<pre><code class="hljs ${escapeHtmlAttribute(validLanguage)}">${highlightedCode}</code></pre>`;
320320
};
321321

322+
renderer.heading = function(text, level, raw) {
323+
let id = raw
324+
.toLowerCase()
325+
.trim()
326+
.replace(/<[^>]*>/g, '')
327+
.replace(/\s+/g, '-')
328+
.replace(/[^\w-]/g, '')
329+
.replace(/-+/g, '-');
330+
if (!id) {
331+
id = `heading-worker-${Math.random().toString(36).substr(2, 9)}`;
332+
}
333+
return `<h${level} id="${id}">${text}</h${level}>`;
334+
};
335+
322336
marked.use({
323337
extensions: [
324338
blockMathExtension,

desktop-app/resources/js/script.js

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ document.addEventListener("DOMContentLoaded", function () {
6161
let syncScrollingEnabled = true;
6262
let isEditorScrolling = false;
6363
let isPreviewScrolling = false;
64+
let isProgrammaticScrolling = false;
6465
let scrollSyncTimeout = null;
6566
const SCROLL_SYNC_DELAY = 10;
6667

@@ -931,6 +932,20 @@ document.addEventListener("DOMContentLoaded", function () {
931932
return `<pre><code class="hljs ${validLanguage}">${highlightedCode}</code></pre>`;
932933
};
933934

935+
renderer.heading = function (text, level, raw) {
936+
let id = raw
937+
.toLowerCase()
938+
.trim()
939+
.replace(/<[^>]*>/g, '')
940+
.replace(/\s+/g, '-')
941+
.replace(/[^\w-]/g, '')
942+
.replace(/-+/g, '-');
943+
if (!id) {
944+
id = 'heading-' + Math.random().toString(36).substr(2, 9);
945+
}
946+
return `<h${level} id="${id}">${text}</h${level}>`;
947+
};
948+
934949
marked.use({
935950
extensions: [
936951
blockMathExtension,
@@ -2921,7 +2936,7 @@ document.addEventListener("DOMContentLoaded", function () {
29212936
}
29222937

29232938
function syncEditorToPreview() {
2924-
if (!syncScrollingEnabled || isPreviewScrolling) return;
2939+
if (!syncScrollingEnabled || isPreviewScrolling || isProgrammaticScrolling) return;
29252940
isEditorScrolling = true;
29262941

29272942
if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
@@ -2944,7 +2959,7 @@ document.addEventListener("DOMContentLoaded", function () {
29442959
}
29452960

29462961
function syncPreviewToEditor() {
2947-
if (!syncScrollingEnabled || isEditorScrolling) return;
2962+
if (!syncScrollingEnabled || isEditorScrolling || isProgrammaticScrolling) return;
29482963
isPreviewScrolling = true;
29492964

29502965
if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
@@ -6583,12 +6598,33 @@ document.addEventListener("DOMContentLoaded", function () {
65836598
.markdown-body {
65846599
box-sizing: border-box;
65856600
min-width: 200px;
6586-
max-width: 980px;
6601+
max-width: 100%;
6602+
width: fit-content;
65876603
margin: 0 auto;
65886604
padding: 45px;
65896605
background-color: ${isDarkTheme ? "#0d1117" : "#ffffff"};
65906606
color: ${isDarkTheme ? "#c9d1d9" : "#24292e"};
65916607
}
6608+
.markdown-body > p,
6609+
.markdown-body > ul,
6610+
.markdown-body > ol,
6611+
.markdown-body > blockquote,
6612+
.markdown-body > h1,
6613+
.markdown-body > h2,
6614+
.markdown-body > h3,
6615+
.markdown-body > h4,
6616+
.markdown-body > h5,
6617+
.markdown-body > h6,
6618+
.markdown-body > pre,
6619+
.markdown-body > table,
6620+
.markdown-body > details,
6621+
.markdown-body > dl,
6622+
.markdown-body > hr {
6623+
max-width: 980px;
6624+
margin-left: auto !important;
6625+
margin-right: auto !important;
6626+
}
6627+
65926628
65936629
/* Syntax Highlighting */
65946630
.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_ { color: ${isDarkTheme ? "#ff7b72" : "#d73a49"}; }
@@ -9646,7 +9682,53 @@ document.addEventListener("DOMContentLoaded", function () {
96469682
const href = link.getAttribute('href');
96479683
if (href) {
96489684
if (href.startsWith('#')) {
9649-
return; // Allow internal anchor navigation
9685+
const targetId = decodeURIComponent(href.slice(1));
9686+
let targetEl = null;
9687+
if (targetId) {
9688+
try {
9689+
targetEl = markdownPreview.querySelector(`[id="${CSS.escape(targetId)}"]`) ||
9690+
markdownPreview.querySelector(`[name="${CSS.escape(targetId)}"]`);
9691+
} catch (err) {
9692+
targetEl = Array.from(markdownPreview.querySelectorAll('[id], [name]')).find(el => {
9693+
return el.getAttribute('id') === targetId || el.getAttribute('name') === targetId;
9694+
});
9695+
}
9696+
9697+
if (!targetEl) {
9698+
const cleanTargetId = targetId.toLowerCase().replace(/[^a-z0-9]/g, '');
9699+
if (cleanTargetId) {
9700+
targetEl = Array.from(markdownPreview.querySelectorAll('h1, h2, h3, h4, h5, h6')).find(heading => {
9701+
const cleanText = heading.textContent.toLowerCase().replace(/[^a-z0-9]/g, '');
9702+
return cleanText === cleanTargetId;
9703+
});
9704+
}
9705+
}
9706+
}
9707+
if (targetEl) {
9708+
e.preventDefault();
9709+
isProgrammaticScrolling = true;
9710+
9711+
// Scroll preview pane to target heading
9712+
targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
9713+
9714+
// Scroll editor pane to the matching synced position
9715+
const previewScrollRange = previewPane.scrollHeight - previewPane.clientHeight;
9716+
const targetRatio = previewScrollRange > 0 ? Math.min(1, Math.max(0, targetEl.offsetTop / previewScrollRange)) : 0;
9717+
const editorScrollPosition = targetRatio * (markdownEditor.scrollHeight - markdownEditor.clientHeight);
9718+
9719+
markdownEditor.scrollTo({
9720+
top: editorScrollPosition,
9721+
behavior: 'smooth'
9722+
});
9723+
9724+
if (window.programmaticScrollTimeout) {
9725+
clearTimeout(window.programmaticScrollTimeout);
9726+
}
9727+
window.programmaticScrollTimeout = setTimeout(() => {
9728+
isProgrammaticScrolling = false;
9729+
}, 1000);
9730+
}
9731+
return;
96509732
}
96519733

96529734
e.preventDefault();

preview-worker.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,20 @@ function configureMarked() {
319319
return `<pre><code class="hljs ${escapeHtmlAttribute(validLanguage)}">${highlightedCode}</code></pre>`;
320320
};
321321

322+
renderer.heading = function(text, level, raw) {
323+
let id = raw
324+
.toLowerCase()
325+
.trim()
326+
.replace(/<[^>]*>/g, '')
327+
.replace(/\s+/g, '-')
328+
.replace(/[^\w-]/g, '')
329+
.replace(/-+/g, '-');
330+
if (!id) {
331+
id = `heading-worker-${Math.random().toString(36).substr(2, 9)}`;
332+
}
333+
return `<h${level} id="${id}">${text}</h${level}>`;
334+
};
335+
322336
marked.use({
323337
extensions: [
324338
blockMathExtension,

script.js

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ document.addEventListener("DOMContentLoaded", function () {
6161
let syncScrollingEnabled = true;
6262
let isEditorScrolling = false;
6363
let isPreviewScrolling = false;
64+
let isProgrammaticScrolling = false;
6465
let scrollSyncTimeout = null;
6566
const SCROLL_SYNC_DELAY = 10;
6667

@@ -931,6 +932,20 @@ document.addEventListener("DOMContentLoaded", function () {
931932
return `<pre><code class="hljs ${validLanguage}">${highlightedCode}</code></pre>`;
932933
};
933934

935+
renderer.heading = function (text, level, raw) {
936+
let id = raw
937+
.toLowerCase()
938+
.trim()
939+
.replace(/<[^>]*>/g, '')
940+
.replace(/\s+/g, '-')
941+
.replace(/[^\w-]/g, '')
942+
.replace(/-+/g, '-');
943+
if (!id) {
944+
id = 'heading-' + Math.random().toString(36).substr(2, 9);
945+
}
946+
return `<h${level} id="${id}">${text}</h${level}>`;
947+
};
948+
934949
marked.use({
935950
extensions: [
936951
blockMathExtension,
@@ -2921,7 +2936,7 @@ document.addEventListener("DOMContentLoaded", function () {
29212936
}
29222937

29232938
function syncEditorToPreview() {
2924-
if (!syncScrollingEnabled || isPreviewScrolling) return;
2939+
if (!syncScrollingEnabled || isPreviewScrolling || isProgrammaticScrolling) return;
29252940
isEditorScrolling = true;
29262941

29272942
if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
@@ -2944,7 +2959,7 @@ document.addEventListener("DOMContentLoaded", function () {
29442959
}
29452960

29462961
function syncPreviewToEditor() {
2947-
if (!syncScrollingEnabled || isEditorScrolling) return;
2962+
if (!syncScrollingEnabled || isEditorScrolling || isProgrammaticScrolling) return;
29482963
isPreviewScrolling = true;
29492964

29502965
if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
@@ -9667,7 +9682,53 @@ document.addEventListener("DOMContentLoaded", function () {
96679682
const href = link.getAttribute('href');
96689683
if (href) {
96699684
if (href.startsWith('#')) {
9670-
return; // Allow internal anchor navigation
9685+
const targetId = decodeURIComponent(href.slice(1));
9686+
let targetEl = null;
9687+
if (targetId) {
9688+
try {
9689+
targetEl = markdownPreview.querySelector(`[id="${CSS.escape(targetId)}"]`) ||
9690+
markdownPreview.querySelector(`[name="${CSS.escape(targetId)}"]`);
9691+
} catch (err) {
9692+
targetEl = Array.from(markdownPreview.querySelectorAll('[id], [name]')).find(el => {
9693+
return el.getAttribute('id') === targetId || el.getAttribute('name') === targetId;
9694+
});
9695+
}
9696+
9697+
if (!targetEl) {
9698+
const cleanTargetId = targetId.toLowerCase().replace(/[^a-z0-9]/g, '');
9699+
if (cleanTargetId) {
9700+
targetEl = Array.from(markdownPreview.querySelectorAll('h1, h2, h3, h4, h5, h6')).find(heading => {
9701+
const cleanText = heading.textContent.toLowerCase().replace(/[^a-z0-9]/g, '');
9702+
return cleanText === cleanTargetId;
9703+
});
9704+
}
9705+
}
9706+
}
9707+
if (targetEl) {
9708+
e.preventDefault();
9709+
isProgrammaticScrolling = true;
9710+
9711+
// Scroll preview pane to target heading
9712+
targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
9713+
9714+
// Scroll editor pane to the matching synced position
9715+
const previewScrollRange = previewPane.scrollHeight - previewPane.clientHeight;
9716+
const targetRatio = previewScrollRange > 0 ? Math.min(1, Math.max(0, targetEl.offsetTop / previewScrollRange)) : 0;
9717+
const editorScrollPosition = targetRatio * (markdownEditor.scrollHeight - markdownEditor.clientHeight);
9718+
9719+
markdownEditor.scrollTo({
9720+
top: editorScrollPosition,
9721+
behavior: 'smooth'
9722+
});
9723+
9724+
if (window.programmaticScrollTimeout) {
9725+
clearTimeout(window.programmaticScrollTimeout);
9726+
}
9727+
window.programmaticScrollTimeout = setTimeout(() => {
9728+
isProgrammaticScrolling = false;
9729+
}, 1000);
9730+
}
9731+
return;
96719732
}
96729733

96739734
e.preventDefault();

0 commit comments

Comments
 (0)