I somehow implemented synced scrollbars
note.tsx:
"use client";
import { useState, useCallback, useRef, useEffect, use } from "react";
import Editor from "./components/editor/editor";
import Preview from "./components/preview/preview";
import { ViewMode } from "./types";
import { Container, NoteResizer, ResizerColumn } from "./style";
import Header from "./components/header/header";
const Note = () => {
const [doc, setDoc] = useState<string>("# Hello, World!n");
const [mode, setMode] = useState<ViewMode>(ViewMode.Middle);
const [width, setWidth] = useState(50);
const [mouseDown, setMouseDown] = useState(false);
const [cursorStyle, setCursorStyle] = useState("auto");
const [left, setLeft] = useState(0);
const [isClicked, setIsClicked] = useState(false);
const menuSize = {
min: 20,
max: 80,
};
const handleDocChange = useCallback((newDoc: string) => {
setDoc(newDoc);
}, []);
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
setMouseDown(true);
setCursorStyle("col-resize");
event.preventDefault();
};
const handleMouseUp = () => {
setMouseDown(false);
setCursorStyle("auto");
};
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (mouseDown) {
const container = event.currentTarget.getBoundingClientRect();
const newWidth = Math.min(
((event.pageX - container.left - 3.5) / container.width) * 100,
menuSize.max
);
if (newWidth > menuSize.min) {
setWidth(newWidth);
}
if (editorRef.current) {
let bc = editorRef.current.getBoundingClientRect();
setLeft(bc.left + bc.width - 5);
}
}
};
useEffect(() => {
const handleResize = () => {
if (editorRef.current) {
let bc = editorRef.current.getBoundingClientRect();
setLeft(bc.left + bc.width - 10);
}
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const firstDivRef = useRef<HTMLDivElement>();
const secondDivRef = useRef<HTMLDivElement>();
const editorRef = useRef<HTMLDivElement>();
const firstCont = useRef<HTMLDivElement>();
const secondCont = useRef<HTMLDivElement>();
useEffect(() => {
if (editorRef.current) {
let bc = editorRef.current.getBoundingClientRect();
setLeft(bc.left + bc.width - 10);
}
}, [editorRef.current, mode]);
const debouncedHandleScrollFirst = (scroll: any) => {
if (
firstDivRef.current &&
secondCont.current &&
firstCont.current &&
secondDivRef.current
) {
secondDivRef.current.scrollTop =
((scroll.target as HTMLDivElement).scrollTop /
firstCont.current.getBoundingClientRect().height) *
secondCont.current.getBoundingClientRect().height;
}
};
const debouncedHandleScrollSecond = (scroll: any) => {
if (
firstDivRef.current &&
secondCont.current &&
firstCont.current &&
secondDivRef.current
) {
firstDivRef.current.scrollTop =
((scroll.target as HTMLDivElement).scrollTop /
secondCont.current.getBoundingClientRect().height) *
firstCont.current.getBoundingClientRect().height;
}
};
return (
<>
<Header setMode={setMode} mode={mode} setWidth={setWidth} />
<Container
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
$cursorStyle={cursorStyle}
>
<Editor
onChange={handleDocChange}
onScroll={debouncedHandleScrollFirst}
initialDoc={doc}
mode={mode}
width={width}
reference={firstDivRef}
reference2={editorRef}
reference3={firstCont}
left={left}
/>
{mode == ViewMode.Middle && (
<NoteResizer onMouseDown={handleMouseDown}>
<ResizerColumn />
</NoteResizer>
)}
<Preview
doc={doc}
mode={mode}
onScroll={debouncedHandleScrollSecond}
reference={secondDivRef}
isClicked={isClicked}
setIsClicked={setIsClicked}
reference2={secondCont}
/>
</Container>
</>
);
};
export default Note;
editor.tsx
import { EditorState } from "@codemirror/state";
import { useCallback, useEffect, useRef, useState } from "react";
import useCodeMirror from "./useCodeMirror";
import { EditorWindow, Scroller, Thumb } from "./style";
import { ViewMode } from "../../types";
interface Props {
initialDoc: string;
mode: ViewMode;
width: number;
onChange: (doc: string) => void;
onScroll: (scroll: Event) => void;
reference: React.MutableRefObject<HTMLDivElement | undefined>;
reference2: React.MutableRefObject<HTMLDivElement | undefined>;
reference3: React.MutableRefObject<HTMLDivElement | undefined>;
left: number;
}
const Editor = (props: Props) => {
const {
onChange,
initialDoc,
mode,
width,
onScroll,
reference,
reference2,
reference3,
left,
} = props;
if (mode !== ViewMode.Preview) {
const [thumbSize, setThumbSize] = useState(5000);
const [distance, setDistance] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [startY, setStartY] = useState(0);
const [startScrollTop, setStartScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement | undefined>(null);
const cntRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!isDragging) {
const handleScroll = (e: Event) => {
if (containerRef.current && cntRef.current) {
const mdHeight =
containerRef.current.getBoundingClientRect().height;
const contHeight = cntRef.current.getBoundingClientRect().height;
//console.log(contHeight * (contHeight / mdHeight));
const distFromTop = (e.target as HTMLDivElement).scrollTop;
setDistance((distFromTop / mdHeight) * (contHeight - 32));
///console.log(contHeight, mdHeight, distFromTop);
}
};
if (reference.current) {
reference.current.addEventListener("scroll", handleScroll);
return () => {
if (reference.current) {
reference.current.removeEventListener("scroll", handleScroll);
}
};
}
}
}, [isDragging, thumbSize]);
const changeThumbSize = () => {
if (containerRef.current && cntRef.current) {
const distFromTop =
(containerRef.current.getBoundingClientRect().top - 69) * -1;
const contHeight = cntRef.current.getBoundingClientRect().height;
const mdHeight = containerRef.current.getBoundingClientRect().height;
setThumbSize(contHeight * (contHeight / mdHeight));
setDistance((distFromTop / mdHeight) * (contHeight - 12));
}
};
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (
!isDragging ||
!cntRef.current ||
!containerRef.current ||
!reference.current
)
return;
const tPos = distance;
const contHeight = containerRef.current.getBoundingClientRect().height;
const mdHeight = cntRef.current.getBoundingClientRect().height;
const newDistance = Math.min(
Math.max(tPos + (e.clientY - startY) - thumbSize, 0),
mdHeight - thumbSize - 12
);
if (e.clientY != startY) {
setDistance(newDistance);
setStartY(newDistance);
}
reference.current.scrollTop = (newDistance / mdHeight) * contHeight;
};
const handleMouseUp = () => {
console.log("mouse up");
setIsDragging(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, startY, startScrollTop, thumbSize, distance]);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(true);
console.log(isDragging);
setStartY(e.clientY);
if (reference.current) {
setStartScrollTop(reference.current.scrollTop);
}
};
const scrollBarClick = (e: React.MouseEvent<HTMLDivElement>) => {
const newDistance = e.clientY - 57 - thumbSize / 2;
if (containerRef.current && cntRef.current && reference.current) {
const contHeight = cntRef.current.clientHeight;
const mdHeight = containerRef.current.getBoundingClientRect().height;
reference.current.scrollTop = (newDistance / contHeight) * mdHeight;
setDistance(
Math.min(
Math.max(newDistance, 0),
containerRef.current.clientHeight - thumbSize
)
);
}
};
const handleChange = useCallback(
(state: EditorState) => {
onChange(state.doc.toString());
changeThumbSize();
},
[onChange]
);
const [refContainer, editorView] = useCodeMirror<HTMLDivElement>({
initialDoc,
onChange: handleChange,
onScroll: onScroll,
reference2: containerRef,
reference: reference,
reference3: reference3,
});
return (
<EditorWindow
$mode={mode}
$width={width}
ref={(node) => {
if (refContainer && node && reference2 && cntRef) {
refContainer.current = node;
reference2.current = node;
cntRef.current = node;
}
}}
>
<Scroller onClick={scrollBarClick} $left={left}>
{thumbSize < (containerRef.current?.clientHeight || 600) && (
<Thumb
$height={thumbSize}
$distance={distance}
onMouseDown={handleMouseDown}
/>
)}
</Scroller>
</EditorWindow>
);
}
};
export default Editor;
codemirror:
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { EditorState } from "@codemirror/state";
import {
EditorView,
keymap,
highlightActiveLine,
lineNumbers,
highlightActiveLineGutter,
drawSelection,
} from "@codemirror/view";
import {
syntaxHighlighting,
HighlightStyle,
indentOnInput,
bracketMatching,
} from "@codemirror/language";
import { tags } from "@lezer/highlight";
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { languages } from "@codemirror/language-data";
import { oneDark } from "@codemirror/theme-one-dark";
import { vim, Vim } from "@replit/codemirror-vim";
interface Props {
initialDoc: string;
onChange?: (state: EditorState) => void;
onScroll: (scroll: Event) => void;
reference: MutableRefObject<HTMLDivElement | undefined>;
reference2: MutableRefObject<HTMLDivElement | undefined | null>;
reference3: React.MutableRefObject<HTMLDivElement | undefined>;
}
const transparentTheme = EditorView.theme({
"&": {
backgroundColor: "transparent !important",
height: "100%",
},
});
const syntaxHighlightingCustom = HighlightStyle.define([
{
tag: tags.heading1,
fontSize: "1.6em",
fontWeight: "bold",
},
{
tag: tags.heading2,
fontSize: "1.4em",
fontWeight: "bold",
},
{
tag: tags.heading3,
fontSize: "1.2em",
fontWeight: "bold",
},
]);
const useCodeMirror = <T extends Element>(
props: Props
): [MutableRefObject<T | null>, EditorView?] => {
const refContainer = useRef<T>(null);
const [editorView, setEditorView] = useState<EditorView>();
//const { onChange, onScroll, reference, onScroll2, reference2 } = props;
const { onChange, onScroll, reference, reference2, reference3 } = props;
useEffect(() => {
if (!refContainer.current) return;
const startState = EditorState.create({
doc: props.initialDoc,
extensions: [
vim(),
keymap.of([...defaultKeymap, ...historyKeymap]),
lineNumbers(),
highlightActiveLineGutter(),
history(),
indentOnInput(),
bracketMatching(),
highlightActiveLine(),
drawSelection(),
markdown({
base: markdownLanguage,
codeLanguages: languages,
addKeymap: true,
}),
oneDark,
transparentTheme,
syntaxHighlighting(syntaxHighlightingCustom),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (update.changes) {
onChange && onChange(update.state);
}
}),
],
});
Vim.map(":", "<Esc>");
const view = new EditorView({
state: startState,
parent: refContainer.current,
});
const scroller = view.dom.querySelector(".cm-scroller");
if (reference && scroller) {
reference.current = scroller as HTMLDivElement;
scroller.addEventListener("scroll", onScroll);
// scroller.addEventListener("scroll", onScroll2);
}
const container = view.dom.querySelector(".cm-content");
if (reference2 && container && reference3) {
reference2.current = container as HTMLDivElement;
reference3.current = container as HTMLDivElement;
}
setEditorView(view);
return () => {
view.destroy();
};
}, [refContainer]);
return [refContainer, editorView];
};
export default useCodeMirror;
preview.tsx:
import { createElement, useEffect, useRef, useState } from "react";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkReact from "remark-react/lib";
import { PreviewWindow, Scroller, Thumb } from "./style";
import RemarkCode from "./remarkCode";
import { defaultSchema } from "hast-util-sanitize";
import "github-markdown-css/github-markdown.css";
import { ViewMode } from "../../types";
interface Props {
doc: string;
mode: ViewMode;
onScroll: (scroll: React.UIEvent<HTMLDivElement>) => void;
reference: React.MutableRefObject<HTMLDivElement | undefined>;
reference2: React.MutableRefObject<HTMLDivElement | undefined>;
setIsClicked: (isClicked: boolean) => void;
isClicked: boolean;
}
const schema = {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: [...(defaultSchema.attributes?.code || []), "className"],
},
};
const Preview = (props: Props) => {
const {
doc,
mode,
onScroll,
reference,
setIsClicked,
isClicked,
reference2,
} = props;
if (mode !== ViewMode.Edit) {
const mdContainerRef = useRef<HTMLDivElement | null>(null);
const ContainerRef = useRef<HTMLDivElement | null>(null);
const [thumbSize, setThumbSize] = useState(5000);
const [distance, setDistance] = useState(0);
const handleScroll = () => {
if (mdContainerRef.current && ContainerRef.current && !isDragging) {
const distFromTop =
(mdContainerRef.current.getBoundingClientRect().top - 69) * -1;
const contHeight = ContainerRef.current.clientHeight;
const mdHeight = mdContainerRef.current.getBoundingClientRect().height;
setDistance((distFromTop / mdHeight) * (contHeight - 32));
}
};
useEffect(() => {
if (mdContainerRef.current) {
const resizeObserver = new ResizeObserver((entries) => {
for (let _ of entries) {
if (mdContainerRef.current && ContainerRef.current) {
const distFromTop =
(mdContainerRef.current.getBoundingClientRect().top - 69) * -1;
const contHeight = ContainerRef.current.clientHeight;
const mdHeight =
mdContainerRef.current.getBoundingClientRect().height;
setThumbSize(contHeight * (contHeight / mdHeight));
setDistance((distFromTop / mdHeight) * (contHeight - 12));
}
}
});
resizeObserver.observe(mdContainerRef.current);
return () => resizeObserver.disconnect();
}
}, []);
useEffect(() => {
if (mdContainerRef.current) {
mdContainerRef.current.addEventListener("scroll", handleScroll);
}
return () => {
if (mdContainerRef.current) {
mdContainerRef.current.removeEventListener("scroll", handleScroll);
}
};
}, []);
const [isDragging, setIsDragging] = useState(false);
const [startY, setStartY] = useState(0);
const [startScrollTop, setStartScrollTop] = useState(0);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(true);
setStartY(e.clientY);
if (mdContainerRef.current) {
setStartScrollTop(mdContainerRef.current.scrollTop);
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
const scrollBarClick = (e: React.MouseEvent<HTMLDivElement>) => {
const newDistance = e.clientY - 57 - thumbSize / 2;
if (ContainerRef.current && mdContainerRef.current) {
const contHeight = ContainerRef.current.clientHeight;
const mdHeight = mdContainerRef.current.getBoundingClientRect().height;
ContainerRef.current.scrollTop = (newDistance / contHeight) * mdHeight;
setDistance(
Math.min(
Math.max(newDistance, 0),
ContainerRef.current.clientHeight - thumbSize
)
);
setIsClicked(true);
}
};
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
if (mdContainerRef.current && ContainerRef.current) {
const tPos = distance;
const contHeight = ContainerRef.current.clientHeight;
const mdHeight =
mdContainerRef.current.getBoundingClientRect().height;
const newDistance = Math.min(
Math.max(tPos + (e.clientY - startY) - thumbSize, 0),
contHeight - thumbSize - 12
);
if (e.clientY != startY) {
setDistance(newDistance);
setStartY(newDistance);
}
ContainerRef.current.scrollTop =
(newDistance / contHeight) * mdHeight;
}
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, startY, startScrollTop, thumbSize, distance]);
const md = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkReact, {
createElement: createElement,
sanitize: schema,
remarkReactComponents: {
code: RemarkCode,
},
} as any)
.processSync(doc).result;
return (
<PreviewWindow
className="markdown-body editor"
onScroll={(e) => {
onScroll(e);
handleScroll();
}}
ref={(node) => {
if (reference && node) {
reference.current = node;
ContainerRef.current = node;
}
}}
>
<Scroller onClick={scrollBarClick}>
{thumbSize < (ContainerRef.current?.clientHeight || 600) && (
<Thumb
$height={thumbSize}
$distance={distance}
onMouseDown={handleMouseDown}
/>
)}
</Scroller>
<div
ref={(node) => {
if (mdContainerRef && node && reference2) {
mdContainerRef.current = node;
reference2.current = node;
}
}}
>
{md as any}
</div>
</PreviewWindow>
);
}
};
export default Preview;
but the problem is that when i start to scroll, they continue to scroll by themselves and i’m not sure how to fix it, i think it happens because im trying to sync them based on height but sync in not correct and they break