From 1e4ef116c638787c758b6f74209d6b3ceec63d8a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:38:16 +0000 Subject: [PATCH 1/3] feat: add embed block node for iframes and JSX components in blog editor Co-Authored-By: Sungbin Jo --- .../admin/blog-editor/embed-block.tsx | 192 ++++++++++++++++++ .../components/admin/blog-editor/index.tsx | 2 + .../components/admin/blog-editor/toolbar.tsx | 16 ++ packages/tiptap/src/shared/embed-block.ts | 88 ++++++++ packages/tiptap/src/shared/index.ts | 1 + 5 files changed, 299 insertions(+) create mode 100644 apps/web/src/components/admin/blog-editor/embed-block.tsx create mode 100644 packages/tiptap/src/shared/embed-block.ts diff --git a/apps/web/src/components/admin/blog-editor/embed-block.tsx b/apps/web/src/components/admin/blog-editor/embed-block.tsx new file mode 100644 index 0000000000..e9aefc23aa --- /dev/null +++ b/apps/web/src/components/admin/blog-editor/embed-block.tsx @@ -0,0 +1,192 @@ +import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"; +import type { NodeViewProps } from "@tiptap/react"; +import { CodeIcon, PencilIcon } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import { EmbedBlockNode as BaseEmbedBlockNode } from "@hypr/tiptap/shared"; + +function EmbedBlockNodeView({ + node, + updateAttributes, + selected, + deleteNode, +}: NodeViewProps) { + const [isEditing, setIsEditing] = useState(!node.attrs.content); + const [inputValue, setInputValue] = useState(node.attrs.content || ""); + const textareaRef = useRef(null); + + useEffect(() => { + setInputValue(node.attrs.content || ""); + }, [node.attrs.content]); + + useEffect(() => { + if (isEditing && textareaRef.current) { + textareaRef.current.focus(); + const el = textareaRef.current; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + } + }, [isEditing]); + + const handleSubmit = useCallback(() => { + const code = inputValue.trim(); + if (!code) return; + updateAttributes({ content: code }); + setIsEditing(false); + }, [inputValue, updateAttributes]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleSubmit(); + } + if (e.key === "Escape") { + e.preventDefault(); + if (node.attrs.content) { + setInputValue(node.attrs.content); + setIsEditing(false); + } + } + if (e.key === "Backspace" && !inputValue && !node.attrs.content) { + e.preventDefault(); + deleteNode(); + } + }, + [handleSubmit, node.attrs.content, inputValue, deleteNode], + ); + + if (isEditing || !node.attrs.content) { + return ( + +
+
+ + + Embed Block + + + Paste iframe, JSX, or HTML + +
+
+