+
+
+ {/* Render the HTML preview in split view mode */}
+ {showSplitView && (
+
+ )}
{showLanguageSelector && (
-
+ {/* Show HTML-specific controls when language is HTML */}
+ {isHtml && (
+
+
+
+ )}
+
+ {/* Group format button and language selector together on the right */}
+
)}
diff --git a/src/frontend/src/pad/editors/HtmlEditor.scss b/src/frontend/src/pad/editors/HtmlEditor.scss
index f414772..3131f26 100644
--- a/src/frontend/src/pad/editors/HtmlEditor.scss
+++ b/src/frontend/src/pad/editors/HtmlEditor.scss
@@ -1,47 +1,4 @@
.html-editor {
- &__container {
- display: flex;
- position: relative;
- width: 100%;
- height: 100%;
- flex-direction: column;
- box-sizing: border-box;
- padding: 32px;
- font-family: sans-serif;
- background: #2d2d2d;
- color: #e0e0e0;
- border-radius: 4px;
- border: 1px solid #484848;
- }
-
- &__content {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- position: relative;
- }
-
- &__monaco-container {
- width: 100%;
- position: relative;
- flex: 1;
- min-height: 0;
- border: 1px solid #484848;
- border-radius: 4px;
- margin-bottom: 10px;
- }
-
- &__controls {
- position: relative;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 0;
- width: 100%;
- flex-shrink: 0;
- }
-
&__label {
font-size: 12px;
color: #e0e0e0;
@@ -58,9 +15,38 @@
border-radius: 4px;
cursor: pointer;
font-size: 12px;
+ margin-right: 8px;
&:hover {
background: #4285e7;
}
+
+ &--active {
+ background: #3a75c4;
+ }
+ }
+
+ &__split-view {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 50%;
+ height: 100%;
+ box-sizing: border-box;
+ z-index: 5;
+ background: #ffffff00;
+ border-left: 2px solid #484848;
+ box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
+ }
+}
+
+// Split view styles for the editor container
+.editor__wrapper.editor__wrapper--split {
+ .monaco-editor {
+ width: 50% !important; /* Force Monaco editor to resize */
+ }
+
+ .editor__container {
+ width: 50% !important; /* Force container to resize */
}
}
diff --git a/src/frontend/src/pad/editors/HtmlEditor.tsx b/src/frontend/src/pad/editors/HtmlEditor.tsx
index 0691d0b..78e1974 100644
--- a/src/frontend/src/pad/editors/HtmlEditor.tsx
+++ b/src/frontend/src/pad/editors/HtmlEditor.tsx
@@ -1,77 +1,93 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useEffect } from 'react';
import type { NonDeleted, ExcalidrawEmbeddableElement } from '@atyrode/excalidraw/element/types';
-import type { AppState } from '@atyrode/excalidraw/types';
-import Editor from './Editor';
import { ExcalidrawElementFactory } from '../../lib/ExcalidrawElementFactory';
+import HtmlPreview from './HtmlPreview';
import './HtmlEditor.scss';
-interface HtmlEditorProps {
- element: NonDeleted
;
- appState: AppState;
- excalidrawAPI?: any;
-}
+// Default HTML content for new HTML elements with API usage documentation as a comment
+export const defaultHtml = `
+`;
-export const HtmlEditor: React.FC = ({
- element,
- appState,
- excalidrawAPI
-}) => {
- const [createNew, setCreateNew] = useState(true);
- const defaultHtml = '';
- const [editorValue, setEditorValue] = useState(
- element.customData?.editorContent || defaultHtml
- );
- const editorRef = useRef(null);
- const elementIdRef = useRef(element.id);
- // Load content from customData when element changes (e.g., when cloned or pasted)
+// Hook to manage HTML editor state and functionality
+export const useHtmlEditor = (
+ element: NonDeleted | undefined,
+ editorRef: React.RefObject,
+ excalidrawAPI?: any,
+ isActive: boolean = true // New parameter to control if the hook is active
+) => {
+ // Always initialize these hooks regardless of isActive
+ const [createNew, setCreateNew] = useState(false);
+ const [showPreview, setShowPreview] = useState(false);
+ const [previewContent, setPreviewContent] = useState('');
+
+ // Update preview content when editor content changes
useEffect(() => {
- // Check if element ID has changed (indicating a new element)
- if (element.id !== elementIdRef.current) {
- elementIdRef.current = element.id;
-
- // If element has customData with editorContent, update the state
- if (element.customData?.editorContent) {
- setEditorValue(element.customData.editorContent);
- } else {
- setEditorValue(defaultHtml);
- }
-
- // Note: We don't need to update language here since HtmlEditor always uses 'html'
- // But we still save it in customData for consistency
+ if (!isActive || !showPreview || !editorRef.current) return;
+
+ try {
+ // Get the current content from the editor
+ const currentContent = editorRef.current.getValue();
+ setPreviewContent(currentContent);
+ } catch (error) {
+ // Handle case where editor model might be disposed
+ console.warn("Could not get editor value:", error);
}
- }, [element.id, element.customData, defaultHtml]);
-
- const handleEditorMount = (editor: any) => {
- editorRef.current = editor;
- };
+ }, [showPreview, editorRef, isActive]);
const applyHtml = () => {
- if (!excalidrawAPI || !editorRef.current) return;
-
- const htmlContent = editorRef.current.getValue();
- const elements = excalidrawAPI.getSceneElements();
-
- // Get the current editor content
- const currentContent = editorRef.current.getValue();
+ if (!isActive || !excalidrawAPI || !editorRef.current || !element) return;
- // Create a new iframe element with the HTML content using our factory
- const newElement = ExcalidrawElementFactory.createIframeElement({
- x: createNew ? element.x + element.width + 20 : element.x,
- y: createNew ? element.y : element.y,
- width: element.width,
- height: element.height,
- htmlContent: htmlContent,
- id: createNew ? undefined : element.id,
- customData: {
- editorContent: currentContent,
- editorLanguage: 'html' // Always set to html for HtmlEditor
+ try {
+ const htmlContent = editorRef.current.getValue();
+
+ // If not creating a new element, show the preview instead
+ if (!createNew) {
+ setPreviewContent(htmlContent);
+ setShowPreview(true);
+ return;
}
- });
-
- // If creating a new element, add it to the scene
- // If updating an existing element, replace it in the scene
- if (createNew) {
+
+ // Otherwise, create a new element as before
+ const elements = excalidrawAPI.getSceneElements();
+
+ // Get the current editor content
+ const currentContent = editorRef.current.getValue();
+
+ // Create a new iframe element with the HTML content using our factory
+ const newElement = ExcalidrawElementFactory.createIframeElement({
+ x: element.x + element.width + 20,
+ y: element.y,
+ width: element.width,
+ height: element.height,
+ htmlContent: htmlContent,
+ id: undefined, // Always create a new element
+ customData: {
+ editorContent: currentContent,
+ editorLanguage: 'html' // Always set to html for HTML content
+ }
+ });
+
+ // Add the new element to the scene
excalidrawAPI.updateScene({
elements: [...elements, newElement]
});
@@ -80,47 +96,89 @@ export const HtmlEditor: React.FC = ({
viewportZoomFactor: 0.95, // Slight zoom out to ensure element is fully visible
animate: true
});
- } else {
- // Replace the existing element
- const updatedElements = elements.map(el =>
- el.id === element.id ? newElement : el
- );
- excalidrawAPI.updateScene({
- elements: updatedElements
- });
+
+ excalidrawAPI.setActiveTool({ type: "selection" });
+ } catch (error) {
+ console.warn("Error applying HTML:", error);
}
+ };
+
+ const togglePreview = () => {
+ if (!isActive) return;
- excalidrawAPI.setActiveTool({ type: "selection" });
+ if (showPreview) {
+ setShowPreview(false);
+ } else {
+ try {
+ const htmlContent = editorRef.current?.getValue() || '';
+ setPreviewContent(htmlContent);
+ setShowPreview(true);
+ } catch (error) {
+ console.warn("Could not toggle preview:", error);
+ }
+ }
};
+ return {
+ createNew,
+ setCreateNew: (value: boolean) => isActive && setCreateNew(value),
+ showPreview,
+ setShowPreview: (value: boolean) => isActive && setShowPreview(value),
+ previewContent,
+ setPreviewContent: (value: string) => isActive && setPreviewContent(value),
+ applyHtml,
+ togglePreview
+ };
+};
+
+
+// HTML-specific toolbar controls component
+export const HtmlEditorControls: React.FC<{
+ createNew: boolean;
+ setCreateNew: (value: boolean) => void;
+ applyHtml: () => void;
+ showPreview?: boolean;
+ togglePreview?: () => void;
+}> = ({ createNew, setCreateNew, applyHtml, showPreview, togglePreview }) => {
+ return (
+ <>
+
+
+
+ {!createNew && togglePreview && (
+
+ )}
+ >
+ );
+};
+
+// Split view component for HTML editor
+export const HtmlEditorSplitView: React.FC<{
+ editorContent: string;
+ previewContent: string;
+ showPreview: boolean;
+}> = ({ editorContent, previewContent, showPreview }) => {
+ if (!showPreview) {
+ return null;
+ }
+
return (
-
-
+
+
);
};
diff --git a/src/frontend/src/pad/editors/HtmlPreview.scss b/src/frontend/src/pad/editors/HtmlPreview.scss
new file mode 100644
index 0000000..72fe021
--- /dev/null
+++ b/src/frontend/src/pad/editors/HtmlPreview.scss
@@ -0,0 +1,18 @@
+.html-preview {
+ &__container {
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ border-radius: 4px;
+ border: 1px solid #484848;
+ background: #fff;
+ position: relative;
+ }
+
+ &__iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+ background: #fff;
+ }
+}
diff --git a/src/frontend/src/pad/editors/HtmlPreview.tsx b/src/frontend/src/pad/editors/HtmlPreview.tsx
new file mode 100644
index 0000000..04affae
--- /dev/null
+++ b/src/frontend/src/pad/editors/HtmlPreview.tsx
@@ -0,0 +1,120 @@
+import React, { useEffect, useRef } from 'react';
+import './HtmlPreview.scss';
+
+// Message passing script to be injected into the iframe
+const messagePassingScript = `
+
+`;
+
+interface HtmlPreviewProps {
+ htmlContent: string;
+ className?: string;
+}
+
+const HtmlPreview: React.FC
= ({
+ htmlContent,
+ className = 'html-preview__container'
+}) => {
+ const iframeRef = useRef(null);
+
+ // Handle API requests from the iframe
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ if (event.data.type === 'API_REQUEST') {
+ // Make the API call using the current user's authentication
+ fetch(event.data.endpoint, {
+ method: event.data.method,
+ credentials: 'include', // This includes the current viewer's cookies
+ body: event.data.method !== 'GET' ? JSON.stringify(event.data.data) : undefined,
+ headers: { 'Content-Type': 'application/json' }
+ })
+ .then(response => {
+ if (!response.ok) throw new Error('API request failed');
+ return response.json();
+ })
+ .then(data => {
+ // Send successful response back to iframe
+ if (iframeRef.current?.contentWindow) {
+ iframeRef.current.contentWindow.postMessage({
+ type: 'API_RESPONSE',
+ requestId: event.data.requestId,
+ data: data
+ }, '*');
+ }
+ })
+ .catch(error => {
+ // Send error back to iframe
+ if (iframeRef.current?.contentWindow) {
+ iframeRef.current.contentWindow.postMessage({
+ type: 'API_RESPONSE',
+ requestId: event.data.requestId,
+ error: error.message
+ }, '*');
+ }
+ });
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ };
+ }, []);
+
+ // Prepare the HTML content with the message passing script
+ const prepareHtmlContent = () => {
+ // Insert the message passing script before the closing body tag
+ // If there's no body tag, append it to the end
+ if (htmlContent.includes('