Skip to content

Commit 3abbcbe

Browse files
committed
cleanup
1 parent b936ebd commit 3abbcbe

File tree

20 files changed

+707
-147
lines changed

20 files changed

+707
-147
lines changed

backend/custom_admin/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@
2424
"@radix-ui/react-toast": "^1.2.4",
2525
"@radix-ui/react-toolbar": "^1.1.1",
2626
"@radix-ui/themes": "^3.1.6",
27+
"@tiptap/core": "^2.10.4",
2728
"@tiptap/extension-color": "^2.10.3",
29+
"@tiptap/extension-focus": "^2.10.4",
30+
"@tiptap/extension-heading": "^2.10.4",
31+
"@tiptap/extension-link": "^2.10.4",
2832
"@tiptap/extension-list-item": "^2.10.3",
33+
"@tiptap/extension-placeholder": "^2.10.4",
2934
"@tiptap/extension-text-align": "^2.10.3",
3035
"@tiptap/extension-text-style": "^2.10.3",
36+
"@tiptap/extension-underline": "^2.10.4",
3137
"@tiptap/pm": "^2.10.3",
3238
"@tiptap/react": "^2.10.3",
3339
"@tiptap/starter-kit": "^2.10.3",

backend/custom_admin/pnpm-lock.yaml

Lines changed: 147 additions & 81 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/custom_admin/src/components/invitation-letter-document-builder/editor-section.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { Box } from "@radix-ui/themes";
1212
import { MoveDown, MoveUp, Pencil, Trash } from "lucide-react";
1313
import { RichEditor } from "../shared/rich-editor";
14+
import { HideNode } from "../shared/rich-editor/menu-bar";
1415
import { useLocalData } from "./local-state";
1516

1617
export const EditorSection = ({
@@ -50,7 +51,11 @@ export const EditorSection = ({
5051
{isPage && <RemovePage onRemove={onRemove} />}
5152
</Flex>
5253
<Box height="var(--space-3)" />
53-
<RichEditor content={content} onUpdate={onUpdate} />
54+
<RichEditor
55+
hide={[HideNode.buttonNode, HideNode.link]}
56+
content={content}
57+
onUpdate={onUpdate}
58+
/>
5459
</Box>
5560
);
5661
};

backend/custom_admin/src/components/shared/base.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Theme } from "@radix-ui/themes";
33
import { DndProvider } from "react-dnd";
44
import { HTML5Backend } from "react-dnd-html5-backend";
55

6-
import "../shared/styles.css";
6+
import "../../custom-styles.css";
77
import clsx from "clsx";
88
import { ArgsProvider } from "./args";
99
import { DjangoAdminEditorProvider } from "./django-admin-editor-modal";
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Box, Dialog, Tooltip } from "@radix-ui/themes";
2+
import { Node, mergeAttributes } from "@tiptap/core";
3+
import { ReactNodeViewRenderer } from "@tiptap/react";
4+
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
5+
import clsx from "clsx";
6+
import { Link, TriangleAlert, XIcon } from "lucide-react";
7+
import { InsertLinkDialogContent } from "./insert-link-dialog";
8+
9+
const ButtonClasses = "rich-editor-button";
10+
11+
const Action = ({
12+
children,
13+
className,
14+
tooltip,
15+
visibleOnHover = false,
16+
...props
17+
}) => (
18+
<Tooltip content={tooltip}>
19+
<Box
20+
style={{
21+
boxShadow: "var(--shadow-3)",
22+
}}
23+
className={clsx(
24+
"rounded-[var(--radius-6)] transition-all absolute text-black bg-white",
25+
{
26+
"group-hover/button:opacity-100 opacity-0": visibleOnHover,
27+
},
28+
className,
29+
)}
30+
p="2"
31+
{...props}
32+
>
33+
{children}
34+
</Box>
35+
</Tooltip>
36+
);
37+
38+
const ButtonComponent = (props) => {
39+
const setLink = (link) => {
40+
props.updateAttributes({
41+
href: link,
42+
});
43+
};
44+
45+
const link = props.node.attrs.href;
46+
47+
return (
48+
<NodeViewWrapper
49+
as="a"
50+
data-type="button"
51+
className={clsx("relative", ButtonClasses)}
52+
>
53+
<Dialog.Root>
54+
<Dialog.Trigger>
55+
<Action
56+
visibleOnHover={!!link}
57+
data-drag-handle
58+
tooltip={link ? "Change link" : "No link set"}
59+
className={clsx(
60+
"top-0 translate-x-[50%] -translate-y-[50%] right-0",
61+
{
62+
"group-[.has-focus]/button:bg-white bg-[var(--yellow-5)]":
63+
!link,
64+
},
65+
)}
66+
>
67+
<Link
68+
size="16"
69+
className={clsx("group-[.has-focus]/button:!block", {
70+
hidden: !link,
71+
})}
72+
/>
73+
74+
<TriangleAlert
75+
size="16"
76+
className={clsx("group-[.has-focus]/button:hidden", {
77+
hidden: !!link,
78+
})}
79+
/>
80+
</Action>
81+
</Dialog.Trigger>
82+
<InsertLinkDialogContent onSubmit={setLink} initialValue={link} />
83+
</Dialog.Root>
84+
85+
<Action
86+
visibleOnHover
87+
onClick={(_) => props.deleteNode()}
88+
tooltip="Remove"
89+
className="top-0 -translate-x-[50%] -translate-y-[50%] left-0 bg-[var(--crimson-5)]"
90+
>
91+
<XIcon size="16" />
92+
</Action>
93+
94+
<NodeViewContent
95+
placeholder="Type label..."
96+
className="text-left before:opacity-40 before:text-white group-[.is-empty]/button:before:!block before:hidden before:h-0 before:opacity-0.5 before:float-left before:content-[attr(placeholder)]"
97+
/>
98+
</NodeViewWrapper>
99+
);
100+
};
101+
102+
export const ButtonNode = Node.create({
103+
name: "buttonNode",
104+
group: "block",
105+
content: "inline*",
106+
selectable: true,
107+
draggable: true,
108+
109+
addAttributes() {
110+
return {
111+
href: "",
112+
};
113+
},
114+
115+
parseHTML() {
116+
return [
117+
{
118+
tag: 'a[data-type="button"]',
119+
},
120+
];
121+
},
122+
123+
renderHTML({ HTMLAttributes }) {
124+
return [
125+
"a",
126+
mergeAttributes(
127+
{
128+
"data-type": "button",
129+
class: ButtonClasses,
130+
},
131+
HTMLAttributes,
132+
),
133+
0,
134+
];
135+
},
136+
137+
addNodeView() {
138+
return ReactNodeViewRenderer(ButtonComponent, {
139+
className: "group/button",
140+
});
141+
},
142+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Link from "@tiptap/extension-link";
2+
3+
export const CustomLink = Link.extend({
4+
parseHTML() {
5+
return [
6+
{
7+
tag: 'a:not([data-type="button"])',
8+
},
9+
];
10+
},
11+
});

backend/custom_admin/src/components/shared/rich-editor/index.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Color } from "@tiptap/extension-color";
2+
import Focus from "@tiptap/extension-focus";
23
import ListItem from "@tiptap/extension-list-item";
4+
import Placeholder from "@tiptap/extension-placeholder";
35
import TextAlign from "@tiptap/extension-text-align";
46
import TextStyle from "@tiptap/extension-text-style";
5-
import { EditorContent, useEditor } from "@tiptap/react";
7+
import Underline from "@tiptap/extension-underline";
8+
import { BubbleMenu, EditorContent, useEditor } from "@tiptap/react";
69
import StarterKit from "@tiptap/starter-kit";
710
import clsx from "clsx";
8-
import { MenuBar } from "./menu-bar";
11+
import { ButtonNode } from "./button-node";
12+
import { CustomLink } from "./custom-link";
13+
import { type HideNode, MenuBar } from "./menu-bar";
914

1015
const extensions = [
1116
Color.configure({ types: [TextStyle.name, ListItem.name] }),
@@ -23,16 +28,34 @@ const extensions = [
2328
TextAlign.configure({
2429
types: ["heading", "paragraph"],
2530
}),
31+
Underline,
32+
ButtonNode,
33+
CustomLink.configure({
34+
openOnClick: false,
35+
autolink: true,
36+
defaultProtocol: "https",
37+
protocols: ["http", "https"],
38+
isAllowedUri: (url, ctx) => {
39+
return true;
40+
},
41+
}),
42+
Placeholder.configure({
43+
showOnlyCurrent: false,
44+
placeholder: "",
45+
}),
46+
Focus,
2647
];
2748

2849
export const RichEditor = ({
2950
content,
3051
onUpdate,
3152
className,
53+
hide,
3254
}: {
3355
content: string;
3456
onUpdate: (content: string) => void;
3557
className?: string;
58+
hide?: HideNode[];
3659
}) => {
3760
const editor = useEditor({
3861
extensions,
@@ -42,14 +65,15 @@ export const RichEditor = ({
4265
},
4366
editorProps: {
4467
attributes: {
45-
class: clsx("outline-none", className),
68+
class: clsx("rich-editor outline-none", className),
4669
},
4770
},
4871
});
4972

5073
return (
5174
<div className="border">
52-
<MenuBar editor={editor} />
75+
<MenuBar hide={hide} editor={editor} />
76+
<BubbleMenu editor={editor}>abc</BubbleMenu>
5377
<EditorContent editor={editor} className="prose max-w-none p-4" />
5478
</div>
5579
);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Button, Dialog, Flex, Text, TextField } from "@radix-ui/themes";
2+
import { useEffect, useState } from "react";
3+
4+
export const InsertLinkDialogContent = ({ onSubmit, initialValue }) => {
5+
const [link, setLink] = useState(initialValue);
6+
const onChange = (e) => setLink(e.target.value);
7+
8+
useEffect(() => {
9+
setLink(initialValue);
10+
}, [initialValue]);
11+
12+
return (
13+
<Dialog.Content maxWidth="450px" aria-describedby={undefined}>
14+
<Dialog.Title>Insert Link</Dialog.Title>
15+
<form action={(_) => onSubmit(link)}>
16+
<Flex direction="column" gap="3">
17+
<label>
18+
<Text as="div" size="2" mb="1" weight="bold">
19+
Link
20+
</Text>
21+
<TextField.Root
22+
defaultValue={link}
23+
placeholder="Name"
24+
type="text"
25+
onChange={onChange}
26+
/>
27+
</label>
28+
</Flex>
29+
<Flex gap="3" mt="4" justify="end" align="center">
30+
<Dialog.Close>
31+
<Button variant="ghost" color="gray">
32+
Close
33+
</Button>
34+
</Dialog.Close>
35+
<Dialog.Close>
36+
<Button variant="soft" type="submit">
37+
Save
38+
</Button>
39+
</Dialog.Close>
40+
</Flex>
41+
</form>
42+
</Dialog.Content>
43+
);
44+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Dialog } from "@radix-ui/themes";
2+
import { Link, Unlink } from "lucide-react";
3+
import { InsertLinkDialogContent } from "./insert-link-dialog";
4+
import { ToolbarButton } from "./toolbar-button";
5+
6+
export const LinkToolbarButton = ({ editor }) => {
7+
const onSubmit = (link) => {
8+
if (!link) {
9+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
10+
return;
11+
}
12+
13+
editor
14+
.chain()
15+
.focus()
16+
.extendMarkRange("link")
17+
.setLink({ href: link })
18+
.run();
19+
};
20+
21+
return (
22+
<Dialog.Root>
23+
<Dialog.Trigger>
24+
<ToolbarButton isActive={editor.isActive("link")} tooltip="Link">
25+
<Link size={16} />
26+
</ToolbarButton>
27+
</Dialog.Trigger>
28+
29+
<ToolbarButton
30+
onClick={() => editor.chain().focus().unsetLink().run()}
31+
disabled={!editor.isActive("link")}
32+
tooltip="Unlink"
33+
>
34+
<Unlink size={16} />
35+
</ToolbarButton>
36+
37+
<InsertLinkDialogContent
38+
onSubmit={onSubmit}
39+
initialValue={editor.getAttributes("link").href}
40+
/>
41+
</Dialog.Root>
42+
);
43+
};

0 commit comments

Comments
 (0)