Prechádzať zdrojové kódy

refactor: improve post editor layout and theme integration

Adam Jafarov 3 týždňov pred
rodič
commit
0d337c795b
2 zmenil súbory, kde vykonal 100 pridanie a 46 odobranie
  1. 1 1
      backend/themes.json
  2. 99 45
      src/components/PostEditor.jsx

+ 1 - 1
backend/themes.json

@@ -1,5 +1,5 @@
 {
-  "activeTheme": "dark",
+  "activeTheme": "forest",
   "customThemes": [],
   "builtInThemes": [
     {

+ 99 - 45
src/components/PostEditor.jsx

@@ -315,60 +315,101 @@ function PostEditor() {
     const previewRef = useRef(null);
     const editorWrapperRef = useRef(null);
     const isScrollingRef = useRef(false);
+    const isTypingRef = useRef(false);
+    const typingTimeoutRef = useRef(null);
+    const rafRef = useRef(null);
 
-    // Scroll Sync: Editor -> Preview (percentage-based)
+    // Mark as typing when content changes (to pause scroll sync)
+    useEffect(() => {
+        if (viewMode !== "split") return;
+
+        isTypingRef.current = true;
+
+        if (typingTimeoutRef.current) {
+            clearTimeout(typingTimeoutRef.current);
+        }
+
+        // Resume scroll sync after 300ms of no typing
+        typingTimeoutRef.current = setTimeout(() => {
+            isTypingRef.current = false;
+        }, 300);
+
+        return () => {
+            if (typingTimeoutRef.current) {
+                clearTimeout(typingTimeoutRef.current);
+            }
+        };
+    }, [formData.content, viewMode]);
+
+    // Scroll Sync: Editor -> Preview (percentage-based with RAF)
     const syncEditorToPreview = () => {
         if (!previewRef.current || !editorWrapperRef.current || viewMode !== "split") return;
-        if (isScrollingRef.current) return;
+        if (isScrollingRef.current || isTypingRef.current) return;
 
         const scrollContainer = editorWrapperRef.current.querySelector('.w-md-editor-area');
         if (!scrollContainer) return;
 
         isScrollingRef.current = true;
-        const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
-        const maxScroll = scrollHeight - clientHeight;
 
-        const previewNode = previewRef.current;
-        const previewMaxScroll = previewNode.scrollHeight - previewNode.clientHeight;
-
-        // If at the end (or very close), snap to end
-        if (maxScroll <= 0 || scrollTop >= maxScroll - 2) {
-            previewNode.scrollTop = previewMaxScroll;
-        } else {
-            const scrollPercent = scrollHeight > clientHeight ? scrollTop / (scrollHeight - clientHeight) : 0;
-            previewNode.scrollTop = scrollPercent * previewMaxScroll;
-        }
+        if (rafRef.current) cancelAnimationFrame(rafRef.current);
+
+        rafRef.current = requestAnimationFrame(() => {
+            const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
+            const maxScroll = scrollHeight - clientHeight;
+
+            const previewNode = previewRef.current;
+            if (!previewNode) return;
 
-        setTimeout(() => { isScrollingRef.current = false; }, 50);
+            const previewMaxScroll = previewNode.scrollHeight - previewNode.clientHeight;
+
+            // If at the end (or very close), snap to end
+            if (maxScroll <= 0 || scrollTop >= maxScroll - 2) {
+                previewNode.scrollTop = previewMaxScroll;
+            } else if (maxScroll > 0) {
+                const scrollPercent = scrollTop / maxScroll;
+                previewNode.scrollTop = scrollPercent * previewMaxScroll;
+            }
+
+            // Shorter lock time for responsiveness
+            setTimeout(() => { isScrollingRef.current = false; }, 16);
+        });
     };
 
-    // Scroll Sync: Preview -> Editor (percentage-based)
+    // Scroll Sync: Preview -> Editor (percentage-based with RAF)
     const syncPreviewToEditor = () => {
         if (!previewRef.current || !editorWrapperRef.current || viewMode !== "split") return;
-        if (isScrollingRef.current) return;
+        if (isScrollingRef.current || isTypingRef.current) return;
 
         isScrollingRef.current = true;
-        const previewNode = previewRef.current;
-        const { scrollTop, scrollHeight, clientHeight } = previewNode;
-        const maxScroll = scrollHeight - clientHeight;
 
-        const scrollContainer = editorWrapperRef.current.querySelector('.w-md-editor-area');
-        if (!scrollContainer) {
-            isScrollingRef.current = false;
-            return;
-        }
+        if (rafRef.current) cancelAnimationFrame(rafRef.current);
 
-        const editorMaxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
+        rafRef.current = requestAnimationFrame(() => {
+            const previewNode = previewRef.current;
+            if (!previewNode) return;
 
-        // If at the end (or very close), snap to end
-        if (maxScroll <= 0 || scrollTop >= maxScroll - 2) {
-            scrollContainer.scrollTop = editorMaxScroll;
-        } else {
-            const scrollPercent = scrollHeight > clientHeight ? scrollTop / (scrollHeight - clientHeight) : 0;
-            scrollContainer.scrollTop = scrollPercent * editorMaxScroll;
-        }
+            const { scrollTop, scrollHeight, clientHeight } = previewNode;
+            const maxScroll = scrollHeight - clientHeight;
+
+            const scrollContainer = editorWrapperRef.current?.querySelector('.w-md-editor-area');
+            if (!scrollContainer) {
+                isScrollingRef.current = false;
+                return;
+            }
+
+            const editorMaxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
 
-        setTimeout(() => { isScrollingRef.current = false; }, 50);
+            // If at the end (or very close), snap to end
+            if (maxScroll <= 0 || scrollTop >= maxScroll - 2) {
+                scrollContainer.scrollTop = editorMaxScroll;
+            } else if (maxScroll > 0) {
+                const scrollPercent = scrollTop / maxScroll;
+                scrollContainer.scrollTop = scrollPercent * editorMaxScroll;
+            }
+
+            // Shorter lock time for responsiveness
+            setTimeout(() => { isScrollingRef.current = false; }, 16);
+        });
     };
 
     // Attach scroll listeners to both containers
@@ -385,12 +426,13 @@ function PostEditor() {
         const handleEditorScroll = () => syncEditorToPreview();
         const handlePreviewScroll = () => syncPreviewToEditor();
 
-        scrollContainer.addEventListener('scroll', handleEditorScroll);
-        preview.addEventListener('scroll', handlePreviewScroll);
+        scrollContainer.addEventListener('scroll', handleEditorScroll, { passive: true });
+        preview.addEventListener('scroll', handlePreviewScroll, { passive: true });
 
         return () => {
             scrollContainer.removeEventListener('scroll', handleEditorScroll);
             preview.removeEventListener('scroll', handlePreviewScroll);
+            if (rafRef.current) cancelAnimationFrame(rafRef.current);
         };
     }, [viewMode]);
 
@@ -405,11 +447,11 @@ function PostEditor() {
 
 
     // Re-sync scroll when images finish loading (handles layout shifts)
+    // Only runs when split mode is active, uses MutationObserver for new images
     useEffect(() => {
         if (viewMode !== "split" || !previewRef.current) return;
 
         const previewNode = previewRef.current;
-        const images = previewNode.querySelectorAll('img');
 
         const handleImageLoad = () => {
             // Small delay to let layout settle
@@ -431,18 +473,30 @@ function PostEditor() {
             }, 100);
         };
 
-        images.forEach(img => {
-            if (!img.complete) {
-                img.addEventListener('load', handleImageLoad);
-            }
+        // Attach to existing images
+        const attachToImages = () => {
+            const images = previewNode.querySelectorAll('img');
+            images.forEach(img => {
+                if (!img.complete && !img.dataset.syncListenerAttached) {
+                    img.dataset.syncListenerAttached = 'true';
+                    img.addEventListener('load', handleImageLoad, { once: true });
+                }
+            });
+        };
+
+        attachToImages();
+
+        // Watch for new images being added
+        const observer = new MutationObserver(() => {
+            attachToImages();
         });
 
+        observer.observe(previewNode, { childList: true, subtree: true });
+
         return () => {
-            images.forEach(img => {
-                img.removeEventListener('load', handleImageLoad);
-            });
+            observer.disconnect();
         };
-    }, [viewMode, sanitizedPreview]);
+    }, [viewMode]); // Only re-run when viewMode changes
 
     return (
         <div className="min-h-screen theme-bg font-sans theme-text selection:bg-blue-100 selection:text-blue-900 flex flex-col transition-colors duration-300">