diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx index beedd2ec..6a39ecc4 100644 --- a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx @@ -13,22 +13,21 @@ import { Code, } from "lucide-react"; + interface TiptapTextProps { content: string; + onContentChange?: (content: string) => void; className?: string; placeholder?: string; - element?: HTMLElement; - tag?: "H1" | "H2" | "H3" | "H4" | "H5" | "H6" | "P" | "SPAN" | "DIV" | any; + } const TiptapText: React.FC = ({ content, - element, onContentChange, className = "", placeholder = "Enter text...", - tag = "p", }) => { const editor = useEditor({ extensions: [StarterKit, Markdown, Underline], @@ -41,6 +40,8 @@ const TiptapText: React.FC = ({ }, }, onBlur: ({ editor }) => { + // const element = editor?.options.element; + // element?.classList.add("tiptap-text-edited"); const markdown = editor?.storage.markdown.getMarkdown(); if (onContentChange) { onContentChange(markdown); @@ -52,10 +53,15 @@ const TiptapText: React.FC = ({ // Update editor content when content prop changes useEffect(() => { - if (editor && content !== editor.getText()) { - editor.commands.setContent(content || placeholder); + if (!editor) return; + // Compare against current plain text to avoid unnecessary updates + const currentText = editor?.storage.markdown.getMarkdown(); + if ((content || "") !== currentText) { + editor.commands.setContent(content || ""); } - }, [content, editor, placeholder]); + }, [content, editor]); + + if (!editor) { return
{content || placeholder}
; diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx index 3037d490..5cf980d2 100644 --- a/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx @@ -3,6 +3,12 @@ import React, { useRef, useEffect, useState, ReactNode } from "react"; import ReactDOM from "react-dom/client"; import TiptapText from "./TiptapText"; +import { useEditor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { Markdown } from "tiptap-markdown"; +import Underline from "@tiptap/extension-underline"; + +const extensions = [StarterKit, Markdown, Underline]; interface TiptapTextReplacerProps { children: ReactNode; @@ -21,10 +27,17 @@ const TiptapTextReplacer: React.FC = ({ slideIndex, onContentChange = () => {}, }) => { + + + const containerRef = useRef(null); const [processedElements, setProcessedElements] = useState( new Set() ); + // Track created React roots to update content when slideData changes + const rootsRef = useRef< + Map + >(new Map()); useEffect(() => { if (!containerRef.current) return; @@ -38,6 +51,7 @@ const TiptapTextReplacer: React.FC = ({ const htmlElement = element as HTMLElement; // Skip if already processed + if ( processedElements.has(htmlElement) || htmlElement.classList.contains("tiptap-text-editor") || @@ -46,6 +60,7 @@ const TiptapTextReplacer: React.FC = ({ return; } + // console.log("htmlElement", htmlElement); // Skip if element is inside an ignored element tree if (isInIgnoredElementTree(htmlElement)) return; @@ -55,10 +70,10 @@ const TiptapTextReplacer: React.FC = ({ // Check if element has meaningful text content if (!trimmedText || trimmedText.length <= 2) return; - + // Skip elements that contain other elements with text (to avoid double processing) if (hasTextChildren(htmlElement)) return; - + // Skip certain element types that shouldn't be editable if (shouldSkipElement(htmlElement)) return; @@ -72,7 +87,7 @@ const TiptapTextReplacer: React.FC = ({ const tiptapContainer = document.createElement("div"); tiptapContainer.style.cssText = allStyles || ""; tiptapContainer.className = Array.from(allClasses).join(" "); - + // Replace the element htmlElement.parentNode?.replaceChild(tiptapContainer, htmlElement); // Mark as processed @@ -80,17 +95,19 @@ const TiptapTextReplacer: React.FC = ({ setProcessedElements((prev) => new Set(prev).add(htmlElement)); // Render TiptapText const root = ReactDOM.createRoot(tiptapContainer); - // Tag the container so we can update just this node on slideData changes - if (dataPath?.path) { - tiptapContainer.setAttribute("data-tiptap-path", dataPath.path); - } - tiptapContainer.setAttribute("data-tiptap-tag", htmlElement.tagName); - tiptapContainer.setAttribute("data-tiptap-value", trimmedText); + const initialContent = dataPath.path + ? getValueByPath(slideData, dataPath.path) ?? trimmedText + : trimmedText; + rootsRef.current.set(tiptapContainer, { + root, + dataPath: dataPath.path, + + fallbackText: trimmedText, + }); root.render( { if (dataPath && onContentChange) { onContentChange(content, dataPath.path, slideIndex); @@ -102,6 +119,34 @@ const TiptapTextReplacer: React.FC = ({ }); }; + + // Replace text elements after a short delay to ensure DOM is ready + const timer = setTimeout(replaceTextElements, 1000); + + return () => { + clearTimeout(timer); + }; + }, [slideData, slideIndex]); + + // When slideData changes, update existing editors' content using the stored dataPath + useEffect(() => { + if (!rootsRef.current || rootsRef.current.size === 0) return; + rootsRef.current.forEach(({ root, dataPath, fallbackText }) => { + const newContent = dataPath ? getValueByPath(slideData, dataPath) ?? fallbackText : fallbackText; + root.render( + { + if (dataPath && onContentChange) { + onContentChange(content, dataPath, slideIndex); + } + }} + placeholder="Enter text..." + /> + ); + }); + }, [slideData, slideIndex]); + // helper functions // Function to check if element is inside an ignored element tree const isInIgnoredElementTree = (element: HTMLElement): boolean => { // List of element types that should be ignored entirely with all their children @@ -189,6 +234,21 @@ const TiptapTextReplacer: React.FC = ({ return false; }; + // Resolve nested values by path like "a.b[0].c" + const getValueByPath = (obj: any, path: string): any => { + if (!obj || !path) return undefined; + const tokens = path + .replace(/\[(\d+)\]/g, ".$1") + .split(".") + .filter(Boolean); + let current: any = obj; + for (const token of tokens) { + if (current == null) return undefined; + current = current[token as keyof typeof current]; + } + return current; + }; + // Helper function to get only direct text content (not from children) const getDirectTextContent = (element: HTMLElement): string => { let text = ""; @@ -296,61 +356,7 @@ const TiptapTextReplacer: React.FC = ({ return { path: "", originalText: "" }; }; - // Replace text elements after a short delay to ensure DOM is ready - const timer = setTimeout(replaceTextElements, 1000); - return () => { - clearTimeout(timer); - }; - }, [slideData, slideIndex]); - - // Update only the changed editors when slideData changes - useEffect(() => { - if (!containerRef.current) return; - - const getNestedValue = (data: any, path: string): string => { - if (!data) return ""; - const keys = path.split(/[.\[\]]+/).filter(Boolean); - let current: any = data; - for (const key of keys) { - if (current == null) return ""; - if (isNaN(Number(key))) current = current[key]; - else current = current[Number(key)]; - } - return typeof current === "string" ? current : ""; - }; - - const nodes = containerRef.current.querySelectorAll( - '[data-tiptap-path]' - ); - nodes.forEach((node) => { - const path = node.getAttribute('data-tiptap-path'); - if (!path) return; - const nextValue = getNestedValue(slideData, path); - const prevValue = node.getAttribute('data-tiptap-value') || ''; - if (nextValue === prevValue) return; - - const root = (node as any).__tiptapRoot as ReactDOM.Root | undefined; - const originalEl = (node as any).__tiptapElement as HTMLElement | undefined; - const tag = node.getAttribute('data-tiptap-tag') || 'P'; - if (!root || !originalEl) return; - - node.setAttribute('data-tiptap-value', nextValue); - root.render( - { - if (path && onContentChange) onContentChange(content, path, slideIndex); - node.setAttribute('data-tiptap-value', content); - }} - placeholder="Enter text..." - /> - ); - }); - }, [slideData, slideIndex, onContentChange]); return (
{children} diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx index f6652e85..bc848f1b 100644 --- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx @@ -32,6 +32,7 @@ export const useGroupLayouts = () => { // Render slide content with group validation, automatic Tiptap text editing, and editable images/icons const renderSlideContent = useMemo(() => { return (slide: any, isEditMode: boolean) => { + const Layout = getGroupLayout(slide.layout, slide.layout_group); if (loading) { return ( diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index 0a65d37c..0bbba51a 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -144,7 +144,7 @@ const PresentationPage: React.FC = ({ isMobilePanelOpen={isMobilePanelOpen} setIsMobilePanelOpen={setIsMobilePanelOpen} /> - +
{ const dispatch = useDispatch(); @@ -69,6 +68,7 @@ export const useAutoSave = ({ // Trigger debounced save debouncedSave(presentationData); + dispatch(addToHistory({ slides: presentationData.slides, actionType: "AUTO_SAVE" @@ -81,7 +81,7 @@ export const useAutoSave = ({ } }; }, [presentationData, enabled, debouncedSave]); - + return { isSaving, }; diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts index 07587878..cb2a6e15 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts @@ -3,7 +3,7 @@ import { useDispatch } from "react-redux"; import { toast } from "sonner"; import { setPresentationData } from "@/store/slices/presentationGeneration"; import { DashboardApi } from '../../services/api/dashboard'; -import { addToHistory } from "@/store/slices/undoRedoSlice"; +import { addToHistory, clearHistory } from "@/store/slices/undoRedoSlice"; export const usePresentationData = ( @@ -18,6 +18,7 @@ export const usePresentationData = ( const data = await DashboardApi.getPresentation(presentationId); if (data) { dispatch(setPresentationData(data)); + dispatch(clearHistory()); dispatch(addToHistory({ slides: data.slides, actionType: "initial_load" diff --git a/servers/nextjs/store/slices/presentationGeneration.ts b/servers/nextjs/store/slices/presentationGeneration.ts index 67c03cbc..9c10e3dd 100644 --- a/servers/nextjs/store/slices/presentationGeneration.ts +++ b/servers/nextjs/store/slices/presentationGeneration.ts @@ -262,8 +262,6 @@ const presentationGenerationSlice = createSlice({ current[Number(finalKey)] = updatedValue; } - // Add debugging - console.log('Redux: Updated slide image at path:', path, 'with URL:', url); }; // Update the slide image