Skip to content

Commit 4f6c301

Browse files
authored
Merge pull request #278 from softnetics/benz/feat/rich-text-hyper-link
feat: add richtext hyperlink extension
2 parents acdead1 + 6a6f28f commit 4f6c301

32 files changed

Lines changed: 914 additions & 112 deletions

.changeset/chatty-readers-knock.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@example/ui-playground": patch
3+
"@genseki/react": patch
4+
---
5+
6+
feat: add richtext hyperlink extension

examples/ui-playground/src/components/slot-before.tsx

Lines changed: 13 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,32 @@
22
import type React from 'react'
33

44
import Color from '@tiptap/extension-color'
5-
import Link from '@tiptap/extension-link'
65
import TextAlign from '@tiptap/extension-text-align'
76
import TextStyle from '@tiptap/extension-text-style'
87
import Underline from '@tiptap/extension-underline'
98
import StarterKit from '@tiptap/starter-kit'
109

1110
import {
11+
CustomImageExtension,
12+
ImageUploadNodeExtension,
13+
ToolbarGroup,
14+
ToolbarSeparator,
15+
} from '@genseki/react'
16+
import {
17+
BackColorExtension,
18+
CustomLinkExtension,
1219
EditorBar,
20+
EditorBgColorPicker,
1321
EditorTextColorPicker,
1422
MarkButton,
1523
RedoButton,
24+
SelectionExtension,
1625
SelectTextStyle,
1726
TextAlignButton,
1827
TextAlignButtonsGroup,
19-
ToolbarGroup,
20-
ToolbarSeparator,
2128
UndoButton,
2229
UploadImageButton,
23-
} from '@genseki/react'
24-
import {
25-
BackColorExtension,
26-
CustomImageExtension,
27-
ImageUploadNodeExtension,
28-
SelectionExtension,
29-
} from '@genseki/react'
30+
} from '@genseki/react/v2'
3031

3132
export const EditorSlotBefore = () => {
3233
return (
@@ -40,6 +41,7 @@ export const EditorSlotBefore = () => {
4041
</ToolbarGroup>
4142
<ToolbarSeparator className="h-auto" />
4243
<EditorTextColorPicker />
44+
<EditorBgColorPicker />
4345
<ToolbarSeparator className="h-auto" />
4446
<ToolbarGroup className="items-center">
4547
<TextAlignButtonsGroup>
@@ -108,66 +110,6 @@ export const editorProviderProps = {
108110
limit: 3,
109111
pathName: 'posts/rich-text',
110112
}),
111-
Link.configure({
112-
openOnClick: false,
113-
autolink: true,
114-
defaultProtocol: 'https',
115-
protocols: ['http', 'https'],
116-
isAllowedUri: (url, ctx) => {
117-
try {
118-
// construct URL
119-
const parsedUrl = url.includes(':')
120-
? new URL(url)
121-
: new URL(`${ctx.defaultProtocol}://${url}`)
122-
123-
// use default validation
124-
if (!ctx.defaultValidate(parsedUrl.href)) {
125-
return false
126-
}
127-
128-
// disallowed protocols
129-
const disallowedProtocols = ['ftp', 'file', 'mailto']
130-
const protocol = parsedUrl.protocol.replace(':', '')
131-
132-
if (disallowedProtocols.includes(protocol)) {
133-
return false
134-
}
135-
136-
// only allow protocols specified in ctx.protocols
137-
const allowedProtocols = ctx.protocols.map((p) => (typeof p === 'string' ? p : p.scheme))
138-
139-
if (!allowedProtocols.includes(protocol)) {
140-
return false
141-
}
142-
143-
// disallowed domains
144-
const disallowedDomains = ['example-phishing.com', 'malicious-site.net']
145-
const domain = parsedUrl.hostname
146-
147-
if (disallowedDomains.includes(domain)) {
148-
return false
149-
}
150-
151-
// all checks have passed
152-
return true
153-
} catch {
154-
return false
155-
}
156-
},
157-
shouldAutoLink: (url) => {
158-
try {
159-
// construct URL
160-
const parsedUrl = url.includes(':') ? new URL(url) : new URL(`https://${url}`)
161-
162-
// only auto-link if the domain is not in the disallowed list
163-
const disallowedDomains = ['example-no-autolink.com', 'another-no-autolink.com']
164-
const domain = parsedUrl.hostname
165-
166-
return !disallowedDomains.includes(domain)
167-
} catch {
168-
return false
169-
}
170-
},
171-
}),
113+
CustomLinkExtension,
172114
],
173115
}

packages/react/src/react/components/compound/editor/components/editor-bar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { useCurrentEditor } from '@tiptap/react'
66
import { cn } from '../../../../utils/cn'
77
import { Toolbar, ToolbarGroup } from '../../../primitives/toolbar'
88

9+
/**
10+
* @deprecated
11+
*/
912
export const EditorBarGroup = ToolbarGroup
1013

14+
/**
15+
* @deprecated
16+
*/
1117
export const EditorBar: React.FC<{ className?: string; children?: React.ReactNode }> = ({
1218
className,
1319
children,
@@ -19,7 +25,7 @@ export const EditorBar: React.FC<{ className?: string; children?: React.ReactNod
1925
return (
2026
<div
2127
className={cn(
22-
'overflow-x-auto self-start sticky top-1 z-10 bg-bg rounded-lg w-full flex items-center [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
28+
'overflow-x-auto self-start sticky top-1 z-11 bg-bg rounded-lg w-full flex items-center [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
2329
className
2430
)}
2531
>

packages/react/src/react/components/compound/editor/components/editor-color-picker.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { type Color as ReactAriaColor, parseColor } from 'react-aria-components'
55
import { SelectionBackgroundIcon } from '@phosphor-icons/react'
66
import { useCurrentEditor } from '@tiptap/react'
77

8-
import { BaseIcon } from '../../../primitives'
98
import { ColorPicker, type ColorPickerProps } from '../../../primitives/color-picker'
109

10+
/**
11+
* @deprecated
12+
*/
1113
export const EditorColorPicker = ({
1214
onPopupClose,
1315
...props
@@ -29,6 +31,9 @@ export const EditorColorPicker = ({
2931
)
3032
}
3133

34+
/**
35+
* @deprecated
36+
*/
3237
export const EditorBgColorPicker = () => {
3338
const { editor } = useCurrentEditor()
3439

@@ -39,12 +44,15 @@ export const EditorBgColorPicker = () => {
3944
onPopupClose={(color) => {
4045
editor.chain().setBackColor(color.toString('hex')).run()
4146
}}
42-
label={<BaseIcon icon={SelectionBackgroundIcon} size="md" weight="duotone" />}
43-
buttonClassName="p-4 border border-border/50 bg-secondary/25"
47+
label={<SelectionBackgroundIcon />}
48+
buttonClassName="p-4 bg-secondary/25"
4449
/>
4550
)
4651
}
4752

53+
/**
54+
* @deprecated
55+
*/
4856
export const EditorTextColorPicker = () => {
4957
const { editor } = useCurrentEditor()
5058

packages/react/src/react/components/compound/editor/components/mark-button.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
} from '@phosphor-icons/react'
1212
import { useCurrentEditor } from '@tiptap/react'
1313

14-
import { BaseIcon } from '../../../primitives/base-icon'
1514
import { ToolbarItem } from '../../../primitives/toolbar'
1615

1716
type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList' | 'link'
@@ -75,14 +74,9 @@ const useMark = (type: MarkType) => {
7574
if (!editor.isActive('link')) {
7675
const { state } = editor
7776
const { from, to } = state.selection
78-
const currentText = state.doc.textBetween(from, to, '')
77+
const url = state.doc.textBetween(from, to, '')
7978

80-
editor.chain().focus().extendMarkRange('link').run()
81-
editor
82-
.chain()
83-
.focus()
84-
.setMark('link', { href: currentText || 'https://' })
85-
.run()
79+
editor.chain().focus().insertContent(`[](${url})`).run()
8680
return
8781
}
8882
editor.chain().focus().unsetMark('link').run()
@@ -93,6 +87,9 @@ const useMark = (type: MarkType) => {
9387
return options[type]
9488
}
9589

90+
/**
91+
* @deprecated
92+
*/
9693
export const MarkButton = (props: { type: MarkType }) => {
9794
const { editor } = useCurrentEditor()
9895
const markOption = useMark(props.type)
@@ -104,11 +101,9 @@ export const MarkButton = (props: { type: MarkType }) => {
104101
size="md"
105102
variant="default"
106103
className="duration-150 ease-out transition-all h-[36px]"
107-
data-selected={markOption.isSelected}
108104
onClick={markOption.onClick}
109-
aria-label={markOption.label}
110105
>
111-
<BaseIcon icon={markOption.icon} weight={markOption.isSelected ? 'bold' : 'regular'} />
106+
<markOption.icon weight={markOption.isSelected ? 'bold' : 'regular'} />
112107
</ToolbarItem>
113108
)
114109
}

packages/react/src/react/components/compound/editor/components/redo-undo-buttons.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { ArrowClockwiseIcon, ArrowCounterClockwiseIcon } from '@phosphor-icons/r
33
import { useCurrentEditor } from '@tiptap/react'
44

55
import { Button } from '../../../../../../v2'
6-
import { BaseIcon } from '../../../primitives/base-icon'
76

7+
/**
8+
* @deprecated
9+
*/
810
export const UndoButton = () => {
911
const { editor } = useCurrentEditor()
1012

@@ -18,14 +20,16 @@ export const UndoButton = () => {
1820
onClick={() => {
1921
editor.chain().focus().undo().run()
2022
}}
21-
aria-label="Undo"
2223
disabled={!editor.can().undo()}
2324
>
24-
<BaseIcon icon={ArrowCounterClockwiseIcon} weight="regular" />
25+
<ArrowCounterClockwiseIcon className="size-8" />
2526
</Button>
2627
)
2728
}
2829

30+
/**
31+
* @deprecated
32+
*/
2933
export const RedoButton = () => {
3034
const { editor } = useCurrentEditor()
3135

@@ -39,10 +43,9 @@ export const RedoButton = () => {
3943
onClick={() => {
4044
editor.chain().focus().redo().run()
4145
}}
42-
aria-label="Redo"
4346
disabled={!editor.can().redo()}
4447
>
45-
<BaseIcon icon={ArrowClockwiseIcon} weight="regular" />
48+
<ArrowClockwiseIcon className="size-8" />
4649
</Button>
4750
)
4851
}

packages/react/src/react/components/compound/editor/components/rich-text-editor.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { EditorProvider } from './rich-text-provider'
99

1010
import { CustomFieldError } from '../../../primitives/custom-field-error'
1111
import { Description } from '../../../primitives/field'
12+
13+
/**
14+
* @deprecated
15+
*/
1216
export interface RichTextEditorProps {
1317
editorProviderProps: EditorProviderProps
1418
value?: string | Content | Content[]
@@ -21,6 +25,9 @@ export interface RichTextEditorProps {
2125
label?: string
2226
}
2327

28+
/**
29+
* @deprecated
30+
*/
2431
export const RichTextEditor = (props: RichTextEditorProps) => {
2532
const editor = useEditor({
2633
...props.editorProviderProps,

packages/react/src/react/components/compound/editor/components/rich-text-provider.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ interface EditorProviderPropsWithEditor extends EditorProviderProps {
1919
}
2020
}
2121

22+
/**
23+
* @deprecated
24+
*/
2225
export function EditorProvider({
2326
children,
2427
slotAfter,

packages/react/src/react/components/compound/editor/components/select-text-style.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ import {
1313
import type { Editor } from '@tiptap/core'
1414
import { useCurrentEditor } from '@tiptap/react'
1515

16-
import { BaseIcon } from '../../../primitives/base-icon'
17-
import { Select, SelectList, SelectOption, SelectTrigger } from '../../../primitives/select'
16+
import {
17+
Select,
18+
SelectContent,
19+
SelectItem,
20+
SelectItemText,
21+
SelectTrigger,
22+
SelectValue,
23+
} from '../../../../../../v2'
1824

1925
const textStylesList = [
2026
{ icon: TextTIcon, label: 'Normal', value: 'p', type: 'paragraph' },
@@ -45,6 +51,9 @@ const getSelectedTextStyle = (editor: Editor) => {
4551
return null
4652
}
4753

54+
/**
55+
* @deprecated
56+
*/
4857
export const SelectTextStyle = () => {
4958
const { editor } = useCurrentEditor()
5059
if (!editor) throw new Error('Editor provider is missing')
@@ -72,22 +81,23 @@ export const SelectTextStyle = () => {
7281

7382
return (
7483
<Select
75-
selectedKey={getSelectedTextStyle(editor)}
76-
className="w-72"
77-
defaultSelectedKey="Normal"
78-
placeholder="Choose style"
79-
aria-label="Select text style"
80-
onSelectionChange={selectChange}
84+
value={getSelectedTextStyle(editor) ?? 'p'}
85+
defaultValue="p"
86+
onValueChange={selectChange}
8187
>
82-
<SelectTrigger className="h-[36px]" />
83-
<SelectList items={textStylesList}>
84-
{(item) => (
85-
<SelectOption key={item.value} id={item.value} textValue={item.value}>
86-
<BaseIcon icon={item.icon} size="sm" weight="regular" />
87-
{item.label}
88-
</SelectOption>
89-
)}
90-
</SelectList>
88+
<SelectTrigger className="w-full">
89+
<SelectValue />
90+
</SelectTrigger>
91+
<SelectContent>
92+
{textStylesList.map((textStyle) => (
93+
<SelectItem key={textStyle.value} value={textStyle.value}>
94+
<SelectItemText className="flex items-center gap-2">
95+
<textStyle.icon />
96+
{textStyle.label}
97+
</SelectItemText>
98+
</SelectItem>
99+
))}
100+
</SelectContent>
91101
</Select>
92102
)
93103
}

0 commit comments

Comments
 (0)