tfrere's picture
tfrere HF Staff
feat(editor): Iframe embed component for remote URLs
0c69852
// ---------------------------------------------------------------------------
// Extension factory
//
// Generates TipTap Node extensions from ComponentDef declarations.
// A single factory handles both "wrapper" (editable children) and
// "atomic" (self-closing) components based on def.kind.
// ---------------------------------------------------------------------------
import { Node, mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import type { ComponentDef } from "./registry";
import { makeWrapperView } from "./WrapperView";
import { makeAtomicView } from "./AtomicView";
import { MermaidNodeView } from "./MermaidView";
import { HfUserNodeView } from "./HfUserView";
import { makeHtmlEmbedView } from "../embeds/HtmlEmbedView";
import { makeIframeEmbedView } from "../embeds/IframeEmbedView";
function buildAttrSchema(fields: ComponentDef["fields"]) {
const attrs: Record<string, { default: unknown }> = {};
for (const f of fields) {
attrs[f.name] = { default: f.default ?? null };
}
return attrs;
}
function buildDefaultAttrs(fields: ComponentDef["fields"]) {
const attrs: Record<string, unknown> = {};
for (const f of fields) {
attrs[f.name] = f.default ?? null;
}
return attrs;
}
/**
* Build a TipTap Node extension for any component definition.
*
* - "wrapper" components have editable ProseMirror content (block+).
* - "atomic" components are self-closing atoms with no inner editing.
*/
export function createComponentExtension(def: ComponentDef) {
const isWrapper = def.kind === "wrapper";
const commandName = `insert${def.tag}`;
return Node.create({
name: def.name,
group: "block",
...(isWrapper
? { content: def.content || "block+", defining: true, isolating: true }
: { atom: true, draggable: true, selectable: true }),
addAttributes() {
return buildAttrSchema(def.fields);
},
parseHTML() {
return [{ tag: `div[data-component="${def.name}"]` }];
},
renderHTML({ HTMLAttributes }) {
const base = mergeAttributes(HTMLAttributes, { "data-component": def.name });
return isWrapper ? ["div", base, 0] : ["div", base];
},
addCommands() {
return {
[commandName]:
() =>
({ commands }: { commands: any }) => {
const content: any = { type: def.name, attrs: buildDefaultAttrs(def.fields) };
if (isWrapper) content.content = [{ type: "paragraph" }];
return commands.insertContent(content);
},
} as any;
},
addNodeView() {
if (isWrapper) {
return ReactNodeViewRenderer(makeWrapperView(def));
}
let View;
if (def.name === "mermaid") View = MermaidNodeView;
else if (def.name === "hfUser") View = HfUserNodeView;
else if (def.name === "htmlEmbed") View = makeHtmlEmbedView(def);
else if (def.name === "iframe") View = makeIframeEmbedView(def);
else View = makeAtomicView(def);
return ReactNodeViewRenderer(View);
},
});
}
/** @deprecated Use createComponentExtension instead */
export const createWrapperExtension = createComponentExtension;
/** @deprecated Use createComponentExtension instead */
export const createAtomicExtension = createComponentExtension;