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, + } +}