|
|
@@ -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">
|