|
|
@@ -11,19 +11,65 @@ interface EditorProps {
|
|
|
let shadow: ShadowRoot | null = null;
|
|
|
let shadowRoot: HTMLDivElement | null = null;
|
|
|
let converter: Converter | null = null;
|
|
|
+let scrollingSide: "edit" | "read" | null = null;
|
|
|
export default function Editor(props: EditorProps) {
|
|
|
const [mode, setMode] = useState(props.allowMode);
|
|
|
+ const [prevMode, setPrevMode] = useState(props.allowMode);
|
|
|
const [displayContent, setDisplayContent] = useState("");
|
|
|
const [convertedContent, setConvertedContent] = useState("");
|
|
|
|
|
|
- // DOM to contain shadow root
|
|
|
- const shadowRootRef = useRef(null);
|
|
|
+ // DOM refs
|
|
|
+ const readViewRef = useRef(null);
|
|
|
+ const editViewRef = useRef(null);
|
|
|
+
|
|
|
+ const checkSyncScroll = (scrollSide: "edit" | "read") => {
|
|
|
+ if (scrollingSide && scrollingSide !== scrollSide) {
|
|
|
+ scrollingSide = null;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ scrollingSide = scrollSide;
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ // Sync scroll on both sides
|
|
|
+ const onScroll = (scrollSide: "edit" | "read") => {
|
|
|
+ // Do not trigger sync on other side
|
|
|
+ if (!checkSyncScroll(scrollSide)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentElement =
|
|
|
+ scrollSide === "read"
|
|
|
+ ? readViewRef.current
|
|
|
+ : editViewRef.current &&
|
|
|
+ (editViewRef.current as HTMLDivElement).querySelector("textarea");
|
|
|
+ if (currentElement) {
|
|
|
+ const currentScrollPosition = (currentElement as HTMLDivElement)
|
|
|
+ .scrollTop;
|
|
|
+ const currentScrollHeight =
|
|
|
+ (currentElement as HTMLDivElement).scrollHeight -
|
|
|
+ (currentElement as HTMLDivElement).clientHeight;
|
|
|
+
|
|
|
+ // Sync scroll ratio
|
|
|
+ const syncElement =
|
|
|
+ scrollSide === "read"
|
|
|
+ ? editViewRef.current &&
|
|
|
+ (editViewRef.current as HTMLDivElement).querySelector("textarea")
|
|
|
+ : readViewRef.current;
|
|
|
+ if (syncElement) {
|
|
|
+ (syncElement as HTMLDivElement).scrollTop =
|
|
|
+ ((syncElement as HTMLDivElement).scrollHeight -
|
|
|
+ (syncElement as HTMLDivElement).clientHeight) *
|
|
|
+ (currentScrollPosition / currentScrollHeight);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
// Render converted content to shadow root
|
|
|
const renderContentToShadow = () => {
|
|
|
- if (shadowRootRef && shadowRootRef.current) {
|
|
|
+ if (readViewRef && readViewRef.current) {
|
|
|
if (!shadow) {
|
|
|
- shadow = (shadowRootRef.current as HTMLDivElement).attachShadow({
|
|
|
+ shadow = (readViewRef.current as HTMLDivElement).attachShadow({
|
|
|
mode: "open",
|
|
|
});
|
|
|
}
|
|
|
@@ -58,10 +104,20 @@ export default function Editor(props: EditorProps) {
|
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
+ // Record previous state
|
|
|
+ // Note: cannot access latest state at global function
|
|
|
+ useEffect(() => {
|
|
|
+ // Sync scroll when switched to both mode
|
|
|
+ if (mode === "both" && prevMode !== "both") {
|
|
|
+ onScroll(prevMode);
|
|
|
+ }
|
|
|
+ setPrevMode(mode);
|
|
|
+ }, [mode]);
|
|
|
+
|
|
|
// Re-render when converted content changes
|
|
|
useEffect(() => {
|
|
|
renderContentToShadow();
|
|
|
- }, [convertedContent, shadowRootRef]);
|
|
|
+ }, [convertedContent, readViewRef]);
|
|
|
|
|
|
// Init conversion
|
|
|
useEffect(() => {
|
|
|
@@ -86,9 +142,18 @@ export default function Editor(props: EditorProps) {
|
|
|
return (
|
|
|
<div className={`pd-editor pd-mode-${mode}`}>
|
|
|
{props.allowMode !== "read" ? (
|
|
|
- <div className="pd-edit-view">
|
|
|
+ <div className="pd-edit-view" ref={editViewRef}>
|
|
|
<textarea
|
|
|
placeholder="Some Markdown here"
|
|
|
+ onScroll={() => {
|
|
|
+ onScroll("edit");
|
|
|
+ }}
|
|
|
+ onPaste={() => {
|
|
|
+ // Sync scroll again after render
|
|
|
+ setTimeout(() => {
|
|
|
+ onScroll("edit");
|
|
|
+ }, 100);
|
|
|
+ }}
|
|
|
onInput={(e) => {
|
|
|
convertText((e.target as HTMLInputElement).value);
|
|
|
}}
|
|
|
@@ -97,7 +162,13 @@ export default function Editor(props: EditorProps) {
|
|
|
</div>
|
|
|
) : null}
|
|
|
{props.allowMode !== "edit" ? (
|
|
|
- <div className="pd-read-view" ref={shadowRootRef} />
|
|
|
+ <div
|
|
|
+ className="pd-read-view"
|
|
|
+ ref={readViewRef}
|
|
|
+ onScroll={() => {
|
|
|
+ onScroll("read");
|
|
|
+ }}
|
|
|
+ />
|
|
|
) : null}
|
|
|
</div>
|
|
|
);
|