Explorar o código

Add sync scroll feature

jerryliao hai 1 ano
pai
achega
4babc3bb80
Modificáronse 3 ficheiros con 104 adicións e 14 borrados
  1. 4 6
      fresh.gen.ts
  2. 78 7
      islands/Editor.tsx
  3. 22 1
      static/global.css

+ 4 - 6
fresh.gen.ts

@@ -2,17 +2,15 @@
 // This file SHOULD be checked into source version control.
 // This file is automatically updated during development when running `dev.ts`.
 
-import * as $0 from "./routes/[name].tsx";
-import * as $1 from "./routes/_app.tsx";
-import * as $2 from "./routes/index.tsx";
+import * as $0 from "./routes/_app.tsx";
+import * as $1 from "./routes/index.tsx";
 import * as $$0 from "./islands/Editor.tsx";
 import * as $$1 from "./islands/TopBar.tsx";
 
 const manifest = {
   routes: {
-    "./routes/[name].tsx": $0,
-    "./routes/_app.tsx": $1,
-    "./routes/index.tsx": $2,
+    "./routes/_app.tsx": $0,
+    "./routes/index.tsx": $1,
   },
   islands: {
     "./islands/Editor.tsx": $$0,

+ 78 - 7
islands/Editor.tsx

@@ -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>
   );

+ 22 - 1
static/global.css

@@ -95,7 +95,7 @@
 
 .pd-editor .pd-edit-view textarea {
   width: 100%;
-  min-height: 100%;
+  height: 100%;
   display: block;
   box-sizing: border-box;
   padding: 0.375rem;
@@ -105,6 +105,27 @@
   outline: none;
 }
 
+.pd-editor .pd-edit-view textarea::-webkit-scrollbar,
+.pd-editor .pd-read-view::-webkit-scrollbar {
+  width: 8px;
+}
+
+.pd-editor .pd-edit-view textarea::-webkit-scrollbar-track,
+.pd-editor .pd-read-view::-webkit-scrollbar-track {
+  background-color: transparent;
+}
+
+.pd-editor .pd-edit-view textarea::-webkit-scrollbar-thumb,
+.pd-editor .pd-read-view::-webkit-scrollbar-thumb {
+  background-color: #d6dee1;
+  border-radius: 8px;
+}
+
+.pd-editor .pd-edit-view textarea::-webkit-scrollbar-thumb:hover,
+.pd-editor .pd-read-view::-webkit-scrollbar-thumb:hover {
+  background-color: #a8bbbf;
+}
+
 .pd-editor .pd-read-view {
   padding: 0.375rem;
 }