feat: hyperlink support for tiptap

This commit is contained in:
Nevo David 2025-08-05 14:05:26 +07:00
parent 2f28814836
commit 57532e15d4
6 changed files with 162 additions and 24 deletions

View File

@ -652,6 +652,11 @@ html[dir='rtl'] [dir='ltr'] {
}
}
.tiptap {
a {
@apply underline;
}
}
.tiptap {
:first-child {
margin-top: 0;

View File

@ -0,0 +1,51 @@
'use client';
import { FC, useCallback } from 'react';
export const AComponent: FC<{
editor: any;
currentValue: string;
}> = ({ editor }) => {
const mark = () => {
const previousUrl = editor?.getAttributes('link')?.href;
const url = window.prompt('URL', previousUrl);
// cancelled
if (url === null) {
return;
}
// empty
if (url === '') {
editor?.chain()?.focus()?.extendMarkRange('link')?.unsetLink()?.run();
return;
}
// update link
try {
editor?.chain()?.focus()?.extendMarkRange('link')?.setLink({ href: url })?.run();
} catch (e) {
}
editor?.commands?.focus();
};
return (
<div
onClick={mark}
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
>
<svg
width="20"
height="20"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.7079 8.29252C17.8008 8.38539 17.8746 8.49568 17.9249 8.61708C17.9752 8.73847 18.0011 8.8686 18.0011 9.00002C18.0011 9.13143 17.9752 9.26156 17.9249 9.38296C17.8746 9.50436 17.8008 9.61465 17.7079 9.70752L9.70786 17.7075C9.61495 17.8004 9.50465 17.8741 9.38325 17.9244C9.26186 17.9747 9.13175 18.0006 9.00036 18.0006C8.86896 18.0006 8.73885 17.9747 8.61746 17.9244C8.49607 17.8741 8.38577 17.8004 8.29286 17.7075C8.19995 17.6146 8.12625 17.5043 8.07596 17.3829C8.02568 17.2615 7.9998 17.1314 7.9998 17C7.9998 16.8686 8.02568 16.7385 8.07596 16.6171C8.12625 16.4957 8.19995 16.3854 8.29286 16.2925L16.2929 8.29252C16.3857 8.19954 16.496 8.12578 16.6174 8.07546C16.7388 8.02513 16.8689 7.99923 17.0004 7.99923C17.1318 7.99923 17.2619 8.02513 17.3833 8.07546C17.5047 8.12578 17.615 8.19954 17.7079 8.29252ZM23.9504 2.05002C23.3003 1.39993 22.5286 0.884251 21.6793 0.532423C20.83 0.180596 19.9197 -0.000488281 19.0004 -0.000488281C18.081 -0.000488281 17.1707 0.180596 16.3214 0.532423C15.4721 0.884251 14.7004 1.39993 14.0504 2.05002L10.2929 5.80627C10.1052 5.99391 9.9998 6.2484 9.9998 6.51377C9.9998 6.77913 10.1052 7.03363 10.2929 7.22127C10.4805 7.40891 10.735 7.51432 11.0004 7.51432C11.2657 7.51432 11.5202 7.40891 11.7079 7.22127L15.4654 3.47127C16.4065 2.55083 17.6726 2.03866 18.989 2.04591C20.3053 2.05316 21.5657 2.57924 22.4966 3.50999C23.4276 4.44074 23.9539 5.70105 23.9613 7.01742C23.9688 8.33379 23.4569 9.6 22.5366 10.5413L18.7779 14.2988C18.5902 14.4862 18.4847 14.7406 18.4846 15.0058C18.4845 15.2711 18.5898 15.5255 18.7772 15.7131C18.9647 15.9008 19.219 16.0063 19.4843 16.0064C19.7495 16.0065 20.004 15.9012 20.1916 15.7138L23.9504 11.95C24.6004 11.3 25.1161 10.5283 25.468 9.67897C25.8198 8.82964 26.0009 7.91933 26.0009 7.00002C26.0009 6.0807 25.8198 5.17039 25.468 4.32107C25.1161 3.47174 24.6004 2.70004 23.9504 2.05002ZM14.2929 18.7775L10.5354 22.535C10.073 23.0078 9.52136 23.3842 8.9125 23.6423C8.30365 23.9004 7.64963 24.0352 6.98832 24.0389C6.32702 24.0425 5.67156 23.9149 5.05989 23.6635C4.44823 23.4121 3.89252 23.0418 3.42494 22.5742C2.95736 22.1065 2.5872 21.5507 2.33589 20.939C2.08458 20.3273 1.95711 19.6718 1.96087 19.0105C1.96463 18.3492 2.09954 17.6952 2.35779 17.0864C2.61603 16.4776 2.99249 15.9261 3.46536 15.4638L7.22161 11.7075C7.40925 11.5199 7.51466 11.2654 7.51466 11C7.51466 10.7347 7.40925 10.4802 7.22161 10.2925C7.03397 10.1049 6.77947 9.99946 6.51411 9.99946C6.24874 9.99946 5.99425 10.1049 5.80661 10.2925L2.05036 14.05C0.737536 15.3628 0 17.1434 0 19C0 20.8566 0.737536 22.6372 2.05036 23.95C3.36318 25.2628 5.14375 26.0004 7.00036 26.0004C8.85697 26.0004 10.6375 25.2628 11.9504 23.95L15.7079 20.1913C15.8953 20.0036 16.0006 19.7492 16.0005 19.4839C16.0004 19.2187 15.8949 18.9644 15.7072 18.7769C15.5196 18.5894 15.2652 18.4842 14.9999 18.4843C14.7347 18.4844 14.4803 18.5899 14.2929 18.7775Z"
fill="currentColor"
/>
</svg>
</div>
);
};

View File

@ -28,10 +28,10 @@ import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { LinkedinCompanyPop } from '@gitroom/frontend/components/launches/helpers/linkedin.component';
import { useDropzone } from 'react-dropzone';
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
import { Dashboard } from '@uppy/react';
import Link from '@tiptap/extension-link';
import {
useEditor,
EditorContent,
@ -53,6 +53,7 @@ import { HeadingComponent } from '@gitroom/frontend/components/new-launch/headin
import Mention from '@tiptap/extension-mention';
import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { AComponent } from '@gitroom/frontend/components/new-launch/a.component';
const InterceptBoldShortcut = Extension.create({
name: 'preventBoldWithUnderline',
@ -521,18 +522,23 @@ export const Editor: FC<{
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
{(editorType === 'markdown' || editorType === 'html') && identifier !== 'telegram' && (
<>
<Bullets
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
<HeadingComponent
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
</>
)}
{(editorType === 'markdown' || editorType === 'html') &&
identifier !== 'telegram' && (
<>
<AComponent
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
<Bullets
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
<HeadingComponent
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
</>
)}
<div
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
onClick={() => setEmojiPickerOpen(!emojiPickerOpen)}
@ -703,6 +709,66 @@ export const OnlyEditor = forwardRef<
InterceptUnderlineShortcut,
BulletList,
ListItem,
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: 'https',
protocols: ['http', 'https'],
isAllowedUri: (url, ctx) => {
try {
// construct URL
const parsedUrl = url.includes(':')
? new URL(url)
: new URL(`${ctx.defaultProtocol}://${url}`);
// use default validation
if (!ctx.defaultValidate(parsedUrl.href)) {
return false;
}
// disallowed protocols
const disallowedProtocols = ['ftp', 'file', 'mailto'];
const protocol = parsedUrl.protocol.replace(':', '');
if (disallowedProtocols.includes(protocol)) {
return false;
}
// only allow protocols specified in ctx.protocols
const allowedProtocols = ctx.protocols.map((p) =>
typeof p === 'string' ? p : p.scheme
);
if (!allowedProtocols.includes(protocol)) {
return false;
}
// all checks have passed
return true;
} catch {
return false;
}
},
shouldAutoLink: (url) => {
try {
// construct URL
const parsedUrl = url.includes(':')
? new URL(url)
: new URL(`https://${url}`);
// only auto-link if the domain is not in the disallowed list
const disallowedDomains = [
'example-no-autolink.com',
'another-no-autolink.com',
];
const domain = parsedUrl.hostname;
return !disallowedDomains.includes(domain);
} catch {
return false;
}
},
}),
...(internal?.integration?.id
? [
Mention.configure({

View File

@ -178,7 +178,13 @@ export const stripHtmlValidation = (
})
.replace(/<p>([.\s\S]*?)<\/p>/g, (match, p1) => {
return `<p>${p1}</p>\n`;
}),
})
.replace(
/<a.*?href="([.\s\S]*?)".*?>([.\s\S]*?)<\/a>/g,
(match, p1, p2) => {
return `<a href="${p1}">[${p2}](${p1})</a>`;
}
),
convertMentionFunction
)
);
@ -203,6 +209,12 @@ export const stripHtmlValidation = (
const processedHtml = convertMention(
convertToAscii(
html
.replace(
/<a.*?href="([.\s\S]*?)".*?>([.\s\S]*?)<\/a>/g,
(match, p1, p2) => {
return `<a href="${p1}">${p1}</a>`;
}
)
.replace(/<ul>/, '\n<ul>')
.replace(/<\/ul>\n/, '</ul>')
.replace(/<li.*?>([.\s\S]*?)<\/li.*?>/gm, (match, p1) => {

View File

@ -85,6 +85,7 @@
"@tiptap/extension-document": "^3.0.6",
"@tiptap/extension-heading": "^3.0.7",
"@tiptap/extension-history": "^3.0.7",
"@tiptap/extension-link": "^3.0.9",
"@tiptap/extension-list": "^3.0.7",
"@tiptap/extension-mention": "^3.0.7",
"@tiptap/extension-paragraph": "^3.0.6",

View File

@ -135,6 +135,9 @@ importers:
'@tiptap/extension-history':
specifier: ^3.0.7
version: 3.0.7(@tiptap/extensions@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
'@tiptap/extension-link':
specifier: ^3.0.9
version: 3.0.9(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
'@tiptap/extension-list':
specifier: ^3.0.7
version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
@ -5914,11 +5917,11 @@ packages:
peerDependencies:
'@tiptap/core': ^3.0.6
'@tiptap/extension-link@3.0.6':
resolution: {integrity: sha512-BQZNnXx52jncbWrS60PXCtL7kYewA9j4XuYj6U+V943mjmXE+pJ8KczF1YZRmbU7YRLRLrGOtMrSUC8ioJpq6Q==}
'@tiptap/extension-link@3.0.9':
resolution: {integrity: sha512-cOsG3vpct7/JuenxCePDj5dlaSUEe2eK/g/jlRixgW4Llx5DvG2yj8+gha4MHdCUp/MrUBR4M+NJk1dOOSKXGw==}
peerDependencies:
'@tiptap/core': ^3.0.6
'@tiptap/pm': ^3.0.6
'@tiptap/core': ^3.0.9
'@tiptap/pm': ^3.0.9
'@tiptap/extension-list-item@3.0.6':
resolution: {integrity: sha512-gu3WJ+7GhIi7gPQuaD59Si1oXjBJHKt9wndLKHjYgzlQZb8pfHvix7MqkdSrF/wY+5ScYm2bZToCZku2baoAJw==}
@ -10949,8 +10952,8 @@ packages:
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkifyjs@4.3.1:
resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==}
linkifyjs@4.3.2:
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
lit-element@4.2.0:
resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==}
@ -22346,11 +22349,11 @@ snapshots:
dependencies:
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
'@tiptap/extension-link@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)':
'@tiptap/extension-link@3.0.9(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)':
dependencies:
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
'@tiptap/pm': 3.0.6
linkifyjs: 4.3.1
linkifyjs: 4.3.2
'@tiptap/extension-list-item@3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))':
dependencies:
@ -22447,7 +22450,7 @@ snapshots:
'@tiptap/extension-heading': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-horizontal-rule': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
'@tiptap/extension-italic': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-link': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
'@tiptap/extension-link': 3.0.9(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
'@tiptap/extension-list': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
'@tiptap/extension-list-item': 3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
'@tiptap/extension-list-keymap': 3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
@ -29153,7 +29156,7 @@ snapshots:
dependencies:
uc.micro: 2.1.0
linkifyjs@4.3.1: {}
linkifyjs@4.3.2: {}
lit-element@4.2.0:
dependencies: