Эх сурвалжийг харах

refactor: major post editor restructure with improved state management

Adam Jafarov 3 долоо хоног өмнө
parent
commit
175e9637c7
1 өөрчлөгдсөн 178 нэмэгдсэн , 111 устгасан
  1. 178 111
      src/components/PostEditor.jsx

+ 178 - 111
src/components/PostEditor.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useMemo } from "react";
+import React, { useState, useEffect, useRef, useMemo } from "react";
 import { useNavigate, useParams, Link } from "react-router-dom";
 import MDEditor, { commands } from "@uiw/react-md-editor";
 import "@uiw/react-md-editor/markdown-editor.css";
@@ -304,12 +304,77 @@ function PostEditor() {
         ADD_ATTR: ["type", "min", "max", "value", "step", "style", "width", "height", "class", "data-line"], // Added data-line
     });
 
-    const previewHtml = renderContent();
-    const sanitizedPreview = getSanitizedHtml(previewHtml);
+    // Memoize markdown rendering to prevent unnecessary re-running
+    const sanitizedPreview = useMemo(() => {
+        return getSanitizedHtml(renderContent());
+    }, [formData.content]);
 
 
     const [settingsOpen, setSettingsOpen] = useState(false);
 
+    const previewRef = useRef(null);
+    const isScrollingRef = useRef(false);
+
+    // Scroll Sync: Editor -> Preview
+    const handleEditorScroll = (e) => {
+        if (!previewRef.current || viewMode !== "split") return;
+        if (isScrollingRef.current) return;
+
+        isScrollingRef.current = true;
+        const textarea = e.target;
+        const { scrollTop, scrollHeight, clientHeight } = textarea;
+
+        const scrollPercent = scrollTop / (scrollHeight - clientHeight);
+        const previewNode = previewRef.current;
+
+        if (previewNode) {
+            previewNode.scrollTop = scrollPercent * (previewNode.scrollHeight - previewNode.clientHeight);
+        }
+
+        setTimeout(() => { isScrollingRef.current = false; }, 50);
+    };
+
+    // Scroll Sync: Preview -> Editor
+    const handlePreviewScroll = (e) => {
+        if (viewMode !== "split") return;
+        if (isScrollingRef.current) return;
+
+        isScrollingRef.current = true;
+        const previewNode = e.target;
+        const { scrollTop, scrollHeight, clientHeight } = previewNode;
+
+        const scrollPercent = scrollTop / (scrollHeight - clientHeight);
+
+        if (editorWrapperRef.current) {
+            const textarea = editorWrapperRef.current.querySelector('textarea');
+            if (textarea) {
+                textarea.scrollTop = scrollPercent * (textarea.scrollHeight - textarea.clientHeight);
+            }
+        }
+
+        setTimeout(() => { isScrollingRef.current = false; }, 50);
+    };
+
+    // Handle Scroll Sync using capture on wrapper since MDEditor might consume scroll on textarea
+    // or better, ensure we target the right element.
+    // The previous textareaProps onScroll should have worked if `MDEditor` passes it.
+    // Let's try attaching it to the editor wrapper with capture to be safe.
+    const editorWrapperRef = useRef(null);
+
+    useEffect(() => {
+        const wrapper = editorWrapperRef.current;
+        if (!wrapper) return;
+
+        // Find the textarea inside the wrapper
+        const textarea = wrapper.querySelector('textarea');
+        if (!textarea) return;
+
+        // Attach scroll listener manually to ensure it's registered
+        const handleScroll = (e) => handleEditorScroll(e);
+        textarea.addEventListener('scroll', handleScroll);
+        return () => textarea.removeEventListener('scroll', handleScroll);
+    }, [viewMode]); // Re-attach if viewMode changes (though refs persist)
+
     // Auto-resize title textarea
     const titleRef = React.useRef(null);
     useEffect(() => {
@@ -475,115 +540,117 @@ function PostEditor() {
                         {/* Split Editor/Preview Layout */}
                         <div className={`grid gap-6 ${viewMode === "split" ? "grid-cols-1 lg:grid-cols-2" : "grid-cols-1"}`}>
 
-                            {/* Editor Column - Visible if write tab active OR split mode */}
-                            {activeTab === "write" || viewMode === "split" ? (
-                                <div className="prose-editor-wrapper -mx-4 md:mx-0">
-                                    <MDEditor
-                                        value={formData.content}
-                                        onChange={handleEditorChange}
-                                        commands={[...commands.getCommands(), ...customCommands]}
-                                        height={viewMode === "split" ? window.innerHeight * 0.8 : window.innerHeight * 0.7}
-                                        minHeight={500}
-                                        preview="edit"
-                                        visibleDragBar={false}
-                                        textareaProps={{
-                                            placeholder: "Tell your story...",
-                                            onPaste: handlePaste,
-                                            onSelect: handleCursorSelect,
-                                            onClick: handleCursorSelect,
-                                            onKeyUp: handleCursorSelect
-                                        }}
-                                        className="shadow-sm"
-                                    />
-                                </div>
-                            ) : null}
-
-                            {/* Preview Column - Visible if preview tab active OR split mode */}
-                            {(activeTab === "preview" || viewMode === "split") && (
-                                <article
-                                    className={`markdown-content prose dark:prose-invert max-w-none prose-lg ${viewMode === "split" ? "border-l pl-6 border-gray-200 dark:border-gray-800" : ""}`}
-                                    style={{
-                                        height: viewMode === "split" ? window.innerHeight * 0.8 : 'auto',
-                                        overflowY: viewMode === "split" ? 'auto' : 'visible'
+                            {/* Editor Column - Keep always mounted but hidden if needed */}
+                            <div
+                                className={`prose-editor-wrapper -mx-4 md:mx-0 ${activeTab !== "write" && viewMode !== "split" ? "hidden" : ""}`}
+                                ref={editorWrapperRef}
+                            >
+                                <MDEditor
+                                    value={formData.content}
+                                    onChange={handleEditorChange}
+                                    commands={[...commands.getCommands(), ...customCommands]}
+                                    height={viewMode === "split" ? window.innerHeight * 0.8 : window.innerHeight * 0.7}
+                                    minHeight={500}
+                                    preview="edit"
+                                    visibleDragBar={false}
+                                    textareaProps={{
+                                        placeholder: "Tell your story...",
+                                        onPaste: handlePaste,
+                                        onSelect: handleCursorSelect,
+                                        onClick: handleCursorSelect,
+                                        onKeyUp: handleCursorSelect,
+                                        // onScroll handled by manual listener on ref
                                     }}
-                                >
-                                    <div
-                                        dangerouslySetInnerHTML={{ __html: sanitizedPreview }}
-                                        ref={(node) => {
-                                            if (node) {
-                                                // Initialize comparison sliders if present in preview
-                                                const sliders = node.querySelectorAll(".comparison-slider");
-                                                sliders.forEach(slider => {
-                                                    slider.oninput = (e) => {
-                                                        const container = e.target.closest(".comparison-wrapper");
-                                                        const topImage = container.querySelector(".comparison-top");
-                                                        const handle = container.querySelector(".slider-handle");
-                                                        const val = e.target.value;
-
-                                                        if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`;
-                                                        if (handle) handle.style.left = `${val}%`;
-                                                    }
-                                                });
-                                                // Initialize Zoom Reels in Preview (Re-using logic from PostView slightly simplified)
-                                                const zoomReels = node.querySelectorAll(".interactive-zoom-reel");
-                                                zoomReels.forEach(reel => {
-                                                    const images = reel.querySelectorAll(".zoom-reel-img");
-                                                    const viewports = reel.querySelectorAll(".zoom-reel-viewport");
-                                                    const slider = reel.querySelector(".zoom-slider");
-                                                    const resetBtn = reel.querySelector(".reset-zoom");
-
-                                                    let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 };
-                                                    const updateTransform = () => {
-                                                        images.forEach(img => {
-                                                            img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`;
-                                                        });
-                                                    };
-
-                                                    slider.oninput = (e) => {
-                                                        state.zoom = parseFloat(e.target.value);
-                                                        if (state.zoom === 1) { state.panX = 0; state.panY = 0; }
-                                                        updateTransform();
-                                                    };
-
-                                                    resetBtn.onclick = () => {
-                                                        state.zoom = 1; state.panX = 0; state.panY = 0; slider.value = 1;
-                                                        updateTransform();
-                                                    };
-
-                                                    // Simplified drag for preview
-                                                    reel.onmousedown = (e) => {
-                                                        if (state.zoom <= 1) return;
-                                                        e.preventDefault();
-                                                        state.isDragging = true;
-                                                        state.startX = e.clientX;
-                                                        state.startY = e.clientY;
-                                                        state.initialPanX = state.panX;
-                                                        state.initialPanY = state.panY;
-                                                        viewports.forEach(v => v.style.cursor = "grabbing");
-                                                    };
-                                                    reel.onmousemove = (e) => {
-                                                        if (!state.isDragging) return;
-                                                        e.preventDefault();
-                                                        const dx = e.clientX - state.startX;
-                                                        const dy = e.clientY - state.startY;
-                                                        state.panX = state.initialPanX + dx;
-                                                        state.panY = state.initialPanY + dy;
-                                                        updateTransform();
-                                                    };
-                                                    reel.onmouseup = () => {
-                                                        state.isDragging = false;
-                                                        viewports.forEach(v => v.style.cursor = "grab");
-                                                    };
-                                                    reel.onmouseleave = () => {
-                                                        state.isDragging = false;
-                                                        viewports.forEach(v => v.style.cursor = "grab");
-                                                    };
-                                                });
-                                            }
-                                        }}
-                                    />
-                                </article>
-                            )}
+                                    className="shadow-sm"
+                                />
+                            </div>
+
+                            {/* Preview Column - Keep always mounted but hidden if needed */}
+                            <article
+                                ref={previewRef}
+                                onScroll={handlePreviewScroll}
+                                className={`markdown-content prose dark:prose-invert max-w-none prose-lg ${viewMode === "split" ? "border-l pl-6 border-gray-200 dark:border-gray-800" : ""} ${activeTab !== "preview" && viewMode !== "split" ? "hidden" : ""}`}
+                                style={{
+                                    height: viewMode === "split" ? window.innerHeight * 0.8 : 'auto',
+                                    overflowY: viewMode === "split" ? 'auto' : 'visible'
+                                }}
+                            >
+                                <div
+                                    dangerouslySetInnerHTML={{ __html: sanitizedPreview }}
+                                    ref={(node) => {
+                                        if (node) {
+                                            // Initialize comparison sliders if present in preview
+                                            const sliders = node.querySelectorAll(".comparison-slider");
+                                            sliders.forEach(slider => {
+                                                slider.oninput = (e) => {
+                                                    const container = e.target.closest(".comparison-wrapper");
+                                                    const topImage = container.querySelector(".comparison-top");
+                                                    const handle = container.querySelector(".slider-handle");
+                                                    const val = e.target.value;
+
+                                                    if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`;
+                                                    if (handle) handle.style.left = `${val}%`;
+                                                }
+                                            });
+                                            // Initialize Zoom Reels in Preview (Re-using logic from PostView slightly simplified)
+                                            const zoomReels = node.querySelectorAll(".interactive-zoom-reel");
+                                            zoomReels.forEach(reel => {
+                                                const images = reel.querySelectorAll(".zoom-reel-img");
+                                                const viewports = reel.querySelectorAll(".zoom-reel-viewport");
+                                                const slider = reel.querySelector(".zoom-slider");
+                                                const resetBtn = reel.querySelector(".reset-zoom");
+
+                                                let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 };
+                                                const updateTransform = () => {
+                                                    images.forEach(img => {
+                                                        img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`;
+                                                    });
+                                                };
+
+                                                slider.oninput = (e) => {
+                                                    state.zoom = parseFloat(e.target.value);
+                                                    if (state.zoom === 1) { state.panX = 0; state.panY = 0; }
+                                                    updateTransform();
+                                                };
+
+                                                resetBtn.onclick = () => {
+                                                    state.zoom = 1; state.panX = 0; state.panY = 0; slider.value = 1;
+                                                    updateTransform();
+                                                };
+
+                                                // Simplified drag for preview
+                                                reel.onmousedown = (e) => {
+                                                    if (state.zoom <= 1) return;
+                                                    e.preventDefault();
+                                                    state.isDragging = true;
+                                                    state.startX = e.clientX;
+                                                    state.startY = e.clientY;
+                                                    state.initialPanX = state.panX;
+                                                    state.initialPanY = state.panY;
+                                                    viewports.forEach(v => v.style.cursor = "grabbing");
+                                                };
+                                                reel.onmousemove = (e) => {
+                                                    if (!state.isDragging) return;
+                                                    e.preventDefault();
+                                                    const dx = e.clientX - state.startX;
+                                                    const dy = e.clientY - state.startY;
+                                                    state.panX = state.initialPanX + dx;
+                                                    state.panY = state.initialPanY + dy;
+                                                    updateTransform();
+                                                };
+                                                reel.onmouseup = () => {
+                                                    state.isDragging = false;
+                                                    viewports.forEach(v => v.style.cursor = "grab");
+                                                };
+                                                reel.onmouseleave = () => {
+                                                    state.isDragging = false;
+                                                    viewports.forEach(v => v.style.cursor = "grab");
+                                                };
+                                            });
+                                        }
+                                    }}
+                                />
+                            </article>
                         </div>
                     </div>
                 </div>