Skip to content

Commit c25ac28

Browse files
authored
Merge pull request #284 from softnetics/benz/fix/rich-text-bug
feat: rich text bug
2 parents e9252e7 + 4bbecda commit c25ac28

8 files changed

Lines changed: 79 additions & 115 deletions

File tree

.changeset/flat-goats-confess.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@genseki/react": patch
3+
---
4+
5+
fix: rich text bug useEffect and z index
6+
fix: rich text link to use prompt instead of markdown

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
EditorBar,
2020
EditorBgColorPicker,
2121
EditorTextColorPicker,
22+
LinkButton,
2223
MarkButton,
2324
RedoButton,
2425
SelectionExtension,
@@ -37,7 +38,7 @@ export const EditorSlotBefore = () => {
3738
<MarkButton type="bold" />
3839
<MarkButton type="italic" />
3940
<MarkButton type="underline" />
40-
<MarkButton type="link" />
41+
<LinkButton />
4142
</ToolbarGroup>
4243
<ToolbarSeparator className="h-auto" />
4344
<EditorTextColorPicker />

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const EditorBar: React.FC<{ className?: string; children?: React.ReactNod
1919
return (
2020
<div
2121
className={cn(
22-
'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',
22+
'overflow-x-auto self-start sticky top-1 z-9 bg-bg rounded-lg w-full flex items-center [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
2323
className
2424
)}
2525
>

packages/react/v2/components/compound/editor/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './editor-bar'
22
export * from './editor-color-picker'
3+
export * from './link-button'
34
export * from './mark-button'
45
export * from './redo-undo-buttons'
56
export * from './rich-text-editor'
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useCallback } from 'react'
2+
3+
import { LinkIcon } from '@phosphor-icons/react'
4+
import { useCurrentEditor } from '@tiptap/react'
5+
6+
import { ToolbarItem } from '@genseki/react'
7+
8+
export const LinkButton = () => {
9+
const { editor } = useCurrentEditor()
10+
const isLinkActive = editor?.isActive('link') ?? false
11+
12+
const handleLinkToggle = useCallback(() => {
13+
if (!editor) return
14+
15+
if (isLinkActive) {
16+
editor.chain().focus().unsetLink().run()
17+
return
18+
}
19+
20+
const { from, to } = editor.state.selection
21+
const selectedText = editor.state.doc.textBetween(from, to, '')
22+
23+
if (!selectedText) {
24+
alert('Please select text first')
25+
return
26+
}
27+
28+
const url = prompt('Enter URL:', '')
29+
30+
if (!url) return
31+
32+
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
33+
}, [editor, isLinkActive])
34+
35+
if (!editor) return null
36+
37+
return (
38+
<ToolbarItem
39+
type="button"
40+
size="md"
41+
variant="default"
42+
className="h-18 transition-all duration-150 ease-out"
43+
data-selected={isLinkActive}
44+
onClick={handleLinkToggle}
45+
aria-label="Link"
46+
>
47+
<LinkIcon weight={isLinkActive ? 'bold' : 'regular'} />
48+
</ToolbarItem>
49+
)
50+
}

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import {
44
type Icon,
5-
LinkIcon,
65
ListBulletsIcon,
76
TextBolderIcon,
87
TextItalicIcon,
@@ -13,7 +12,7 @@ import { useCurrentEditor } from '@tiptap/react'
1312

1413
import { ToolbarItem } from '../../../../../src/react/components/primitives/toolbar'
1514

16-
type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList' | 'link'
15+
type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList'
1716

1817
type MarkOptions = Record<
1918
MarkType,
@@ -66,22 +65,6 @@ const useMark = (type: MarkType) => {
6665
editor.chain().focus().toggleBulletList().run()
6766
},
6867
},
69-
link: {
70-
label: 'Link',
71-
icon: LinkIcon,
72-
isSelected: editor.isActive('link'),
73-
onClick() {
74-
if (!editor.isActive('link')) {
75-
const { state } = editor
76-
const { from, to } = state.selection
77-
const url = state.doc.textBetween(from, to, '')
78-
79-
editor.chain().focus().insertContent(`[](${url})`).run()
80-
return
81-
}
82-
editor.chain().focus().unsetMark('link').run()
83-
},
84-
},
8568
}
8669

8770
return options[type]
@@ -97,7 +80,7 @@ export const MarkButton = (props: { type: MarkType }) => {
9780
<ToolbarItem
9881
size="md"
9982
variant="default"
100-
className="duration-150 ease-out transition-all h-[36px]"
83+
className="duration-150 ease-out transition-all h-18"
10184
onClick={markOption.onClick}
10285
type="button"
10386
>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ export const RichTextEditor = (props: RichTextEditorProps) => {
3737
if (props.value !== undefined && !isDeepEqual(editor.getJSON(), props.value)) {
3838
editor.commands.setContent(props.value)
3939
}
40+
}, [editor, props.value])
41+
42+
useEffect(() => {
43+
if (!editor) return
4044
editor.setEditable(!props.isDisabled)
41-
}, [editor, props.value, props.isDisabled])
45+
}, [editor, props.isDisabled])
4246

4347
return (
4448
<div className="flex flex-col gap-y-4 w-full" data-invalid={true}>
Lines changed: 12 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,15 @@
1-
import { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'
2-
import type { LinkOptions } from '@tiptap/extension-link'
3-
import { Link } from '@tiptap/extension-link'
1+
import Link from '@tiptap/extension-link'
42

5-
const inputRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["](.+)["])?\)$/i
6-
7-
const pasteRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["](.+)["])?\)/gi
8-
9-
function linkInputRule(config: Parameters<typeof markInputRule>[0]) {
10-
const defaultMarkInputRule = markInputRule(config)
11-
12-
return new InputRule({
13-
find: config.find,
14-
handler(props) {
15-
const { tr } = props.state
16-
17-
defaultMarkInputRule.handler(props)
18-
tr.setMeta('preventAutolink', true)
19-
},
20-
})
21-
}
22-
23-
function linkPasteRule(config: Parameters<typeof markPasteRule>[0]) {
24-
const defaultMarkPasteRule = markPasteRule(config)
25-
26-
return new PasteRule({
27-
find: config.find,
28-
handler(props) {
29-
const { tr } = props.state
30-
31-
defaultMarkPasteRule.handler(props)
32-
tr.setMeta('preventAutolink', true)
33-
},
34-
})
35-
}
36-
37-
type CustomLinkExtensionOptions = LinkOptions
38-
39-
const CustomLinkExtension = Link.extend<CustomLinkExtensionOptions>({
3+
export const CustomLinkExtension = Link.extend({
404
inclusive: false,
41-
addOptions() {
42-
return {
43-
...this.parent?.(),
44-
openOnClick: 'whenNotEditable',
45-
}
46-
},
47-
addAttributes() {
48-
return {
49-
...this.parent?.(),
50-
title: {
51-
default: null,
52-
},
53-
}
54-
},
55-
56-
addInputRules() {
57-
return [
58-
linkInputRule({
59-
find: inputRegex,
60-
type: this.type,
61-
62-
// We need to use `pop()` to remove the last capture groups from the match to
63-
// satisfy Tiptap's `markPasteRule` expectation of having the content as the last
64-
// capture group in the match (this makes the attribute order important)
65-
getAttributes(match) {
66-
return {
67-
title: match.pop()?.trim(),
68-
href: match.pop()?.trim(),
69-
}
70-
},
71-
}),
72-
]
73-
},
74-
addPasteRules() {
75-
return [
76-
linkPasteRule({
77-
find: pasteRegex,
78-
type: this.type,
79-
80-
// We need to use `pop()` to remove the last capture groups from the match to
81-
// satisfy Tiptap's `markInputRule` expectation of having the content as the last
82-
// capture group in the match (this makes the attribute order important)
83-
getAttributes(match) {
84-
return {
85-
title: match.pop()?.trim(),
86-
href: match.pop()?.trim(),
87-
}
88-
},
89-
}),
90-
]
91-
},
5+
}).configure({
6+
openOnClick: false,
7+
HTMLAttributes: {
8+
class: 'custom-link',
9+
rel: 'noopener noreferrer',
10+
target: '_blank',
11+
},
12+
protocols: ['http', 'https', 'mailto', 'tel'],
13+
autolink: true,
14+
linkOnPaste: true,
9215
})
93-
94-
export { CustomLinkExtension }
95-
96-
export type { CustomLinkExtensionOptions }

0 commit comments

Comments
 (0)