146 lines
4.1 KiB
TypeScript
146 lines
4.1 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { Node, mergeAttributes } from "@tiptap/react";
|
||
|
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||
|
|
import type { NodeViewProps } from "@tiptap/react";
|
||
|
|
import { Star } from "lucide-react";
|
||
|
|
import * as React from "react";
|
||
|
|
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
import { NodeViewWrapper } from "@tiptap/react";
|
||
|
|
|
||
|
|
export type DividerStyle = "solid" | "dashed" | "dotted" | "dots" | "star";
|
||
|
|
|
||
|
|
function lineClass(style: DividerStyle) {
|
||
|
|
switch (style) {
|
||
|
|
case "solid":
|
||
|
|
return "h-px bg-border";
|
||
|
|
case "dashed":
|
||
|
|
return "h-0 w-full border-t-2 border-dashed border-border";
|
||
|
|
case "dotted":
|
||
|
|
return "h-0 w-full border-t-2 border-dotted border-border";
|
||
|
|
case "dots":
|
||
|
|
return "hidden";
|
||
|
|
case "star":
|
||
|
|
return "h-px w-full bg-border/70";
|
||
|
|
default:
|
||
|
|
return "h-px bg-border";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function DividerView(props: NodeViewProps) {
|
||
|
|
const { node, updateAttributes, selected } = props;
|
||
|
|
const style = (node.attrs.style as DividerStyle) ?? "solid";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<NodeViewWrapper
|
||
|
|
className={cn(
|
||
|
|
"divider-block group relative my-6 flex min-h-[28px] items-center justify-center py-1",
|
||
|
|
selected &&
|
||
|
|
"rounded ring-2 ring-primary/40 ring-offset-2 ring-offset-background",
|
||
|
|
)}
|
||
|
|
data-type="divider"
|
||
|
|
data-divider-style={style}
|
||
|
|
contentEditable={false}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2",
|
||
|
|
lineClass(style),
|
||
|
|
)}
|
||
|
|
aria-hidden
|
||
|
|
/>
|
||
|
|
{style === "dots" ? (
|
||
|
|
<div className="relative flex items-center justify-center gap-2 px-8">
|
||
|
|
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/45" />
|
||
|
|
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/45" />
|
||
|
|
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/45" />
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
{style === "star" ? (
|
||
|
|
<span
|
||
|
|
className="relative z-[1] inline-flex size-8 items-center justify-center rounded-full border border-border bg-background text-primary shadow-sm dark:bg-card"
|
||
|
|
contentEditable={false}
|
||
|
|
>
|
||
|
|
<Star className="size-4 fill-primary/15 text-primary" aria-hidden />
|
||
|
|
</span>
|
||
|
|
) : style !== "dots" ? (
|
||
|
|
<span className="sr-only">Divider</span>
|
||
|
|
) : null}
|
||
|
|
<select
|
||
|
|
aria-label="Divider style"
|
||
|
|
className={cn(
|
||
|
|
"absolute right-0 top-1/2 z-10 -translate-y-1/2 cursor-pointer rounded border border-border bg-background px-1 py-0.5 text-[10px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
|
||
|
|
"focus:opacity-100",
|
||
|
|
)}
|
||
|
|
value={style}
|
||
|
|
onChange={(e) =>
|
||
|
|
updateAttributes({ style: e.target.value as DividerStyle })
|
||
|
|
}
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
>
|
||
|
|
<option value="solid">Solid</option>
|
||
|
|
<option value="dashed">Dashed</option>
|
||
|
|
<option value="dotted">Dotted</option>
|
||
|
|
<option value="dots">Dots</option>
|
||
|
|
<option value="star">Star</option>
|
||
|
|
</select>
|
||
|
|
</NodeViewWrapper>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export const Divider = Node.create({
|
||
|
|
name: "divider",
|
||
|
|
group: "block",
|
||
|
|
atom: true,
|
||
|
|
draggable: true,
|
||
|
|
selectable: true,
|
||
|
|
|
||
|
|
addAttributes() {
|
||
|
|
return {
|
||
|
|
style: {
|
||
|
|
default: "solid" as DividerStyle,
|
||
|
|
parseHTML: (el) =>
|
||
|
|
(el.getAttribute("data-divider-style") as DividerStyle | null) ??
|
||
|
|
"solid",
|
||
|
|
renderHTML: (attrs) => ({
|
||
|
|
"data-divider-style": attrs.style ?? "solid",
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
parseHTML() {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
tag: 'div[data-type="divider"]',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
},
|
||
|
|
|
||
|
|
renderHTML({ HTMLAttributes }) {
|
||
|
|
return [
|
||
|
|
"div",
|
||
|
|
mergeAttributes(HTMLAttributes, {
|
||
|
|
"data-type": "divider",
|
||
|
|
role: "separator",
|
||
|
|
}),
|
||
|
|
];
|
||
|
|
},
|
||
|
|
|
||
|
|
addNodeView() {
|
||
|
|
return ReactNodeViewRenderer(DividerView);
|
||
|
|
},
|
||
|
|
|
||
|
|
addKeyboardShortcuts() {
|
||
|
|
return {
|
||
|
|
"Mod-Shift-Alt-d": () =>
|
||
|
|
this.editor.commands.insertContent({
|
||
|
|
type: this.name,
|
||
|
|
attrs: { style: "solid" },
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
},
|
||
|
|
});
|