135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { Node, mergeAttributes } from "@tiptap/react";
|
||
|
|
import {
|
||
|
|
NodeViewContent,
|
||
|
|
NodeViewWrapper,
|
||
|
|
ReactNodeViewRenderer,
|
||
|
|
type NodeViewProps,
|
||
|
|
} from "@tiptap/react";
|
||
|
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||
|
|
import * as React from "react";
|
||
|
|
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
function ToggleView(props: NodeViewProps) {
|
||
|
|
const { node, updateAttributes, selected } = props;
|
||
|
|
const open = Boolean(node.attrs.open);
|
||
|
|
const title = typeof node.attrs.title === "string" ? node.attrs.title : "";
|
||
|
|
|
||
|
|
const onTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
updateAttributes({ title: e.target.value });
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<NodeViewWrapper
|
||
|
|
as="details"
|
||
|
|
className={cn(
|
||
|
|
"toggle-block my-2 rounded-lg border border-border bg-card/60 dark:bg-card/40",
|
||
|
|
selected && "ring-2 ring-primary/40 ring-offset-2 ring-offset-background",
|
||
|
|
)}
|
||
|
|
data-type="toggle"
|
||
|
|
open={open}
|
||
|
|
onToggle={(e: React.SyntheticEvent<HTMLDetailsElement>) => {
|
||
|
|
updateAttributes({ open: e.currentTarget.open });
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<summary
|
||
|
|
className="flex cursor-pointer list-none items-center gap-2 px-2 py-1.5 text-[15px] font-medium text-foreground marker:hidden [&::-webkit-details-marker]:hidden"
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
className="flex size-6 shrink-0 items-center justify-center text-muted-foreground"
|
||
|
|
contentEditable={false}
|
||
|
|
>
|
||
|
|
{open ? (
|
||
|
|
<ChevronDown className="size-4" aria-hidden />
|
||
|
|
) : (
|
||
|
|
<ChevronRight className="size-4" aria-hidden />
|
||
|
|
)}
|
||
|
|
</span>
|
||
|
|
<input
|
||
|
|
className="min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
|
||
|
|
placeholder="Toggle"
|
||
|
|
type="text"
|
||
|
|
value={title}
|
||
|
|
onChange={onTitleChange}
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
/>
|
||
|
|
</summary>
|
||
|
|
<div
|
||
|
|
className="toggle-body border-t border-border px-2 py-2 pl-9 [&_.ProseMirror_p]:my-1"
|
||
|
|
data-toggle-body="true"
|
||
|
|
>
|
||
|
|
<NodeViewContent />
|
||
|
|
</div>
|
||
|
|
</NodeViewWrapper>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export const Toggle = Node.create({
|
||
|
|
name: "toggle",
|
||
|
|
group: "block",
|
||
|
|
content: "block+",
|
||
|
|
defining: true,
|
||
|
|
|
||
|
|
addAttributes() {
|
||
|
|
return {
|
||
|
|
open: {
|
||
|
|
default: true,
|
||
|
|
parseHTML: (el) =>
|
||
|
|
el instanceof HTMLElement && el.hasAttribute("open"),
|
||
|
|
renderHTML: (attrs) => (attrs.open ? { open: "" } : {}),
|
||
|
|
},
|
||
|
|
title: {
|
||
|
|
default: "",
|
||
|
|
parseHTML: (el) => {
|
||
|
|
if (!(el instanceof HTMLElement)) return "";
|
||
|
|
const s = el.querySelector(":scope > summary");
|
||
|
|
return s?.textContent?.trim() ?? "";
|
||
|
|
},
|
||
|
|
renderHTML: (attrs) => ({}),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
parseHTML() {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
tag: 'details[data-type="toggle"]',
|
||
|
|
contentElement: 'div[data-toggle-body="true"]',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
},
|
||
|
|
|
||
|
|
renderHTML({ node, HTMLAttributes }) {
|
||
|
|
return [
|
||
|
|
"details",
|
||
|
|
mergeAttributes(HTMLAttributes, {
|
||
|
|
"data-type": "toggle",
|
||
|
|
...(node.attrs.open ? { open: "" } : {}),
|
||
|
|
}),
|
||
|
|
[
|
||
|
|
"summary",
|
||
|
|
{ class: "toggle-summary" },
|
||
|
|
node.attrs.title ? String(node.attrs.title) : "\u00a0",
|
||
|
|
],
|
||
|
|
["div", { "data-toggle-body": "true", class: "toggle-body" }, 0],
|
||
|
|
];
|
||
|
|
},
|
||
|
|
|
||
|
|
addNodeView() {
|
||
|
|
return ReactNodeViewRenderer(ToggleView);
|
||
|
|
},
|
||
|
|
|
||
|
|
addKeyboardShortcuts() {
|
||
|
|
return {
|
||
|
|
"Mod-Shift-Alt-t": () =>
|
||
|
|
this.editor.commands.insertContent({
|
||
|
|
type: this.name,
|
||
|
|
attrs: { open: true, title: "" },
|
||
|
|
content: [{ type: "paragraph" }],
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
},
|
||
|
|
});
|