Skip to content

Commit 00b4dc4

Browse files
committed
Get links working in editor
1 parent 4cc622c commit 00b4dc4

File tree

3 files changed

+168
-69
lines changed

3 files changed

+168
-69
lines changed

src/components/ui/coaching-sessions/coaching-notes/extensions.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,77 @@ export const Extensions = (
4444
Strike,
4545
Text,
4646
Underline,
47-
Link,
47+
Link.configure({
48+
openOnClick: false,
49+
autolink: true,
50+
defaultProtocol: "https",
51+
protocols: ["http", "https"],
52+
isAllowedUri: (url, ctx) => {
53+
try {
54+
// construct URL
55+
const parsedUrl = url.includes(":")
56+
? new URL(url)
57+
: new URL(`${ctx.defaultProtocol}://${url}`);
58+
59+
// use default validation
60+
if (!ctx.defaultValidate(parsedUrl.href)) {
61+
return false;
62+
}
63+
64+
// disallowed protocols
65+
const disallowedProtocols = ["ftp", "file", "mailto"];
66+
const protocol = parsedUrl.protocol.replace(":", "");
67+
68+
if (disallowedProtocols.includes(protocol)) {
69+
return false;
70+
}
71+
72+
// only allow protocols specified in ctx.protocols
73+
const allowedProtocols = ctx.protocols.map((p) =>
74+
typeof p === "string" ? p : p.scheme
75+
);
76+
77+
if (!allowedProtocols.includes(protocol)) {
78+
return false;
79+
}
80+
81+
// disallowed domains
82+
const disallowedDomains = [
83+
"example-phishing.com",
84+
"malicious-site.net",
85+
];
86+
const domain = parsedUrl.hostname;
87+
88+
if (disallowedDomains.includes(domain)) {
89+
return false;
90+
}
91+
92+
// all checks have passed
93+
return true;
94+
} catch {
95+
return false;
96+
}
97+
},
98+
shouldAutoLink: (url) => {
99+
try {
100+
// construct URL
101+
const parsedUrl = url.includes(":")
102+
? new URL(url)
103+
: new URL(`https://${url}`);
104+
105+
// only auto-link if the domain is not in the disallowed list
106+
const disallowedDomains = [
107+
"example-no-autolink.com",
108+
"another-no-autolink.com",
109+
];
110+
const domain = parsedUrl.hostname;
111+
112+
return !disallowedDomains.includes(domain);
113+
} catch {
114+
return false;
115+
}
116+
},
117+
}),
48118
Collaboration.configure({
49119
document: doc,
50120
}),
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from "react";
2+
import { Editor } from "@tiptap/core";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from "@/components/ui/dialog";
11+
import { Label } from "@/components/ui/label";
12+
import { Input } from "@/components/ui/input";
13+
import { Button } from "@/components/ui/button";
14+
15+
interface LinkDialogProps {
16+
editor: Editor;
17+
isOpen: boolean;
18+
onOpenChange: (open: boolean) => void;
19+
}
20+
21+
export const LinkDialog = ({
22+
editor,
23+
isOpen,
24+
onOpenChange,
25+
}: LinkDialogProps) => {
26+
const [linkUrl, setLinkUrl] = React.useState("");
27+
28+
React.useEffect(() => {
29+
if (isOpen) {
30+
setLinkUrl(editor.getAttributes("link").href || "");
31+
}
32+
}, [isOpen, editor]);
33+
34+
const setLink = React.useCallback(() => {
35+
// cancelled
36+
if (linkUrl === null) {
37+
return;
38+
}
39+
40+
// empty
41+
if (linkUrl === "") {
42+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
43+
onOpenChange(false);
44+
return;
45+
}
46+
47+
// update link
48+
try {
49+
editor
50+
.chain()
51+
.focus()
52+
.extendMarkRange("link")
53+
.setLink({ href: linkUrl })
54+
.run();
55+
setLinkUrl("");
56+
onOpenChange(false);
57+
} catch (e) {
58+
console.error("Error inserting link:", e);
59+
}
60+
}, [editor, linkUrl, onOpenChange]);
61+
62+
return (
63+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
64+
<DialogContent>
65+
<DialogHeader>
66+
<DialogTitle>Insert Link</DialogTitle>
67+
<DialogDescription>
68+
Insert a url for a link in your document.
69+
</DialogDescription>
70+
</DialogHeader>
71+
<div className="grid grid-cols-6 gap-4 items-center">
72+
<Label htmlFor="url" className="text-right">
73+
URL
74+
</Label>
75+
<Input
76+
id="url"
77+
value={linkUrl}
78+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
79+
setLinkUrl(e.target.value)
80+
}
81+
className="col-span-5"
82+
/>
83+
</div>
84+
<DialogFooter>
85+
<Button onClick={setLink}>Submit</Button>
86+
</DialogFooter>
87+
</DialogContent>
88+
</Dialog>
89+
);
90+
};

src/components/ui/coaching-sessions/coaching-notes/toolbar.tsx

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,17 @@ import {
1515
Link,
1616
} from "lucide-react";
1717
import { Button } from "@/components/ui/button";
18-
import {
19-
Dialog,
20-
DialogContent,
21-
DialogDescription,
22-
DialogFooter,
23-
DialogHeader,
24-
DialogTitle,
25-
} from "@/components/ui/dialog";
26-
import { Label } from "@/components/ui/label";
27-
import { Input } from "@/components/ui/input";
2818
import { useState } from "react";
19+
import { LinkDialog } from "./link-dialog";
2920

3021
export const Toolbar = () => {
3122
const { editor } = useCurrentEditor();
32-
3323
const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
34-
const [linkUrl, setLinkUrl] = useState("");
35-
const [linkText, setLinkText] = useState("");
3624

3725
if (!editor) {
3826
return null;
3927
}
4028

41-
const handleLinkButtonClick = () => {
42-
editor.isActive("link")
43-
? editor.chain().focus().toggleLink({ href: linkUrl }).run()
44-
: setIsLinkDialogOpen(true);
45-
};
46-
4729
return (
4830
<>
4931
<div className="flex items-center gap-0 mt-1 mx-1 mb-0">
@@ -120,61 +102,18 @@ export const Toolbar = () => {
120102
title="Code Block"
121103
/>
122104
<ToolbarButton
123-
onClick={() => handleLinkButtonClick()}
105+
onClick={() => setIsLinkDialogOpen(true)}
124106
isActive={editor.isActive("link")}
125107
icon={<Link className="h-4 w-4" />}
126108
title="Link"
127109
/>
128110
</div>
129111

130-
<Dialog open={isLinkDialogOpen} onOpenChange={setIsLinkDialogOpen}>
131-
<DialogContent>
132-
<DialogHeader>
133-
<DialogTitle>Insert Link</DialogTitle>
134-
</DialogHeader>
135-
<div className="grid gap-4 py-4">
136-
<div className="grid grid-cols-4 items-center gap-4">
137-
<Label htmlFor="linkText" className="text-right">
138-
Text
139-
</Label>
140-
<Input
141-
id="linkText"
142-
value={linkText}
143-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
144-
setLinkText(e.target.value)
145-
}
146-
className="col-span-3"
147-
/>
148-
</div>
149-
<div className="grid grid-cols-4 items-center gap-4">
150-
<Label htmlFor="url" className="text-right">
151-
URL
152-
</Label>
153-
<Input
154-
id="url"
155-
value={linkUrl}
156-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
157-
setLinkUrl(e.target.value)
158-
}
159-
className="col-span-3"
160-
/>
161-
</div>
162-
</div>
163-
<DialogFooter>
164-
<Button
165-
onClick={() => {
166-
editor.chain().focus().insertContent(linkText).run();
167-
editor.chain().focus().toggleLink({ href: linkUrl }).run();
168-
setIsLinkDialogOpen(false);
169-
setLinkUrl("");
170-
setLinkText("");
171-
}}
172-
>
173-
Insert Link
174-
</Button>
175-
</DialogFooter>
176-
</DialogContent>
177-
</Dialog>
112+
<LinkDialog
113+
editor={editor}
114+
isOpen={isLinkDialogOpen}
115+
onOpenChange={setIsLinkDialogOpen}
116+
/>
178117
</>
179118
);
180119
};

0 commit comments

Comments
 (0)