diff --git a/src/components/MarkdownUi.vue b/src/components/MarkdownUi.vue
index 8aa79cd9..dde36ae7 100644
--- a/src/components/MarkdownUi.vue
+++ b/src/components/MarkdownUi.vue
@@ -26,6 +26,7 @@
autocomplete="off"
autocorrect="off"
class="markdown-editor-textarea"
+ :class="[scrollableClass]"
data-testid="markdown-editor-textarea"
placeholder="Begin editing..."
spellcheck="false"
@@ -43,7 +44,10 @@
class="markdown-content-container"
data-testid="markdown-content-container"
>
-
+
@@ -51,7 +55,7 @@
diff --git a/src/composables/index.ts b/src/composables/index.ts
index 78463c8f..4da2598e 100644
--- a/src/composables/index.ts
+++ b/src/composables/index.ts
@@ -3,6 +3,7 @@ import useKeyboardShortcuts from './useKeyboardShortcuts'
import useMarkdownActions from './useMarkdownActions'
import useMarkdownIt from './useMarkdownIt'
import useShikiji from './useShikiji'
+import useSyncScroll from './useSyncScroll'
export default {
useDebounce,
@@ -10,4 +11,5 @@ export default {
useMarkdownActions,
useMarkdownIt,
useShikiji,
+ useSyncScroll,
}
diff --git a/src/composables/useKeyboardShortcuts.ts b/src/composables/useKeyboardShortcuts.ts
index ec743e19..638d45b0 100644
--- a/src/composables/useKeyboardShortcuts.ts
+++ b/src/composables/useKeyboardShortcuts.ts
@@ -6,7 +6,7 @@ import { KEYBOARD_SHORTCUTS } from '../constants'
import type { InlineFormat } from '../types'
/**
- * Utilize keyboard shortcuts in the markdown editor.
+ * Utilize keyboard shortcuts in the markdown editor. Must be called at the root of the `setup` function.
* @param {string} textareaId The `id` of the textarea
* @param {Ref} rawMarkdown A Vue ref containing the raw markdown content from the textarea.
* @param {Function} onEditCallback A function to call after toggling the inline text formatting.
diff --git a/src/composables/useSyncScroll.ts b/src/composables/useSyncScroll.ts
new file mode 100644
index 00000000..d7e40444
--- /dev/null
+++ b/src/composables/useSyncScroll.ts
@@ -0,0 +1,48 @@
+import type { Ref } from 'vue'
+
+export default function useSyncScroll(scrollableClass: Ref) {
+ // Keep scroll containers in sync
+ const handleScroll = (e: Event) => {
+ const syncScroll = (scrolledEle: Element, ele: Element) => {
+ const scrolledPercent = scrolledEle.scrollTop / (scrolledEle.scrollHeight - scrolledEle.clientHeight)
+ const top = scrolledPercent * (ele.scrollHeight - ele.clientHeight)
+
+ const scrolledWidthPercent = scrolledEle.scrollLeft / (scrolledEle.scrollWidth - scrolledEle.clientWidth)
+ const left = scrolledWidthPercent * (ele.scrollWidth - ele.clientWidth)
+
+ ele.scrollTo({
+ behavior: 'instant', // must be instant
+ top,
+ left,
+ })
+ }
+
+ const scrolledEle = e.target
+
+ Array.from([...document.querySelectorAll(`.${scrollableClass.value}`)]).filter((item) => item !== scrolledEle).forEach((ele: Element) => {
+ ele.removeEventListener('scroll', handleScroll)
+ // @ts-ignore
+ syncScroll(scrolledEle, ele)
+ window.requestAnimationFrame(() => {
+ ele.addEventListener('scroll', handleScroll)
+ })
+ })
+ }
+
+ const initializeSyncScroll = (): void => {
+ document?.querySelectorAll(`.${scrollableClass.value}`)?.forEach((el) => {
+ el?.addEventListener('scroll', handleScroll)
+ })
+ }
+
+ const destroySyncScroll = (): void => {
+ document?.querySelectorAll(`.${scrollableClass.value}`)?.forEach((el) => {
+ el?.removeEventListener('scroll', handleScroll)
+ })
+ }
+
+ return {
+ initializeSyncScroll,
+ destroySyncScroll,
+ }
+}