Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/flat-goats-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@genseki/react": patch
---

fix: rich text bug useEffect and z index
fix: rich text link to use prompt instead of markdown
3 changes: 2 additions & 1 deletion examples/ui-playground/src/components/slot-before.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
EditorBar,
EditorBgColorPicker,
EditorTextColorPicker,
LinkButton,
MarkButton,
RedoButton,
SelectionExtension,
Expand All @@ -37,7 +38,7 @@ export const EditorSlotBefore = () => {
<MarkButton type="bold" />
<MarkButton type="italic" />
<MarkButton type="underline" />
<MarkButton type="link" />
<LinkButton />
</ToolbarGroup>
<ToolbarSeparator className="h-auto" />
<EditorTextColorPicker />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const EditorBar: React.FC<{ className?: string; children?: React.ReactNod
return (
<div
className={cn(
'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',
'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',
className
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './editor-bar'
export * from './editor-color-picker'
export * from './link-button'
export * from './mark-button'
export * from './redo-undo-buttons'
export * from './rich-text-editor'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useCallback } from 'react'

import { LinkIcon } from '@phosphor-icons/react'
import { useCurrentEditor } from '@tiptap/react'

import { ToolbarItem } from '@genseki/react'

export const LinkButton = () => {
const { editor } = useCurrentEditor()
const isLinkActive = editor?.isActive('link') ?? false

const handleLinkToggle = useCallback(() => {
if (!editor) return

if (isLinkActive) {
editor.chain().focus().unsetLink().run()
return
}

const { from, to } = editor.state.selection
const selectedText = editor.state.doc.textBetween(from, to, '')

if (!selectedText) {
alert('Please select text first')
return
}

const url = prompt('Enter URL:', '')

if (!url) return

editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}, [editor, isLinkActive])

if (!editor) return null

return (
<ToolbarItem
type="button"
size="md"
variant="default"
className="h-18 transition-all duration-150 ease-out"
data-selected={isLinkActive}
onClick={handleLinkToggle}
aria-label="Link"
>
<LinkIcon weight={isLinkActive ? 'bold' : 'regular'} />
</ToolbarItem>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import {
type Icon,
LinkIcon,
ListBulletsIcon,
TextBolderIcon,
TextItalicIcon,
Expand All @@ -13,7 +12,7 @@ import { useCurrentEditor } from '@tiptap/react'

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

type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList' | 'link'
type MarkType = 'bold' | 'italic' | 'underline' | 'strike' | 'bulletList'

type MarkOptions = Record<
MarkType,
Expand Down Expand Up @@ -66,22 +65,6 @@ const useMark = (type: MarkType) => {
editor.chain().focus().toggleBulletList().run()
},
},
link: {
label: 'Link',
icon: LinkIcon,
isSelected: editor.isActive('link'),
onClick() {
if (!editor.isActive('link')) {
const { state } = editor
const { from, to } = state.selection
const url = state.doc.textBetween(from, to, '')

editor.chain().focus().insertContent(`[](${url})`).run()
return
}
editor.chain().focus().unsetMark('link').run()
},
},
}

return options[type]
Expand All @@ -97,7 +80,7 @@ export const MarkButton = (props: { type: MarkType }) => {
<ToolbarItem
size="md"
variant="default"
className="duration-150 ease-out transition-all h-[36px]"
className="duration-150 ease-out transition-all h-18"
onClick={markOption.onClick}
type="button"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ export const RichTextEditor = (props: RichTextEditorProps) => {
if (props.value !== undefined && !isDeepEqual(editor.getJSON(), props.value)) {
editor.commands.setContent(props.value)
}
}, [editor, props.value])

useEffect(() => {
if (!editor) return
editor.setEditable(!props.isDisabled)
}, [editor, props.value, props.isDisabled])
}, [editor, props.isDisabled])

return (
<div className="flex flex-col gap-y-4 w-full" data-invalid={true}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,96 +1,15 @@
import { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'
import type { LinkOptions } from '@tiptap/extension-link'
import { Link } from '@tiptap/extension-link'
import Link from '@tiptap/extension-link'

const inputRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)$/i

const pasteRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)/gi

function linkInputRule(config: Parameters<typeof markInputRule>[0]) {
const defaultMarkInputRule = markInputRule(config)

return new InputRule({
find: config.find,
handler(props) {
const { tr } = props.state

defaultMarkInputRule.handler(props)
tr.setMeta('preventAutolink', true)
},
})
}

function linkPasteRule(config: Parameters<typeof markPasteRule>[0]) {
const defaultMarkPasteRule = markPasteRule(config)

return new PasteRule({
find: config.find,
handler(props) {
const { tr } = props.state

defaultMarkPasteRule.handler(props)
tr.setMeta('preventAutolink', true)
},
})
}

type CustomLinkExtensionOptions = LinkOptions

const CustomLinkExtension = Link.extend<CustomLinkExtensionOptions>({
export const CustomLinkExtension = Link.extend({
inclusive: false,
addOptions() {
return {
...this.parent?.(),
openOnClick: 'whenNotEditable',
}
},
addAttributes() {
return {
...this.parent?.(),
title: {
default: null,
},
}
},

addInputRules() {
return [
linkInputRule({
find: inputRegex,
type: this.type,

// We need to use `pop()` to remove the last capture groups from the match to
// satisfy Tiptap's `markPasteRule` expectation of having the content as the last
// capture group in the match (this makes the attribute order important)
getAttributes(match) {
return {
title: match.pop()?.trim(),
href: match.pop()?.trim(),
}
},
}),
]
},
addPasteRules() {
return [
linkPasteRule({
find: pasteRegex,
type: this.type,

// We need to use `pop()` to remove the last capture groups from the match to
// satisfy Tiptap's `markInputRule` expectation of having the content as the last
// capture group in the match (this makes the attribute order important)
getAttributes(match) {
return {
title: match.pop()?.trim(),
href: match.pop()?.trim(),
}
},
}),
]
},
}).configure({
openOnClick: false,
HTMLAttributes: {
class: 'custom-link',
rel: 'noopener noreferrer',
target: '_blank',
},
protocols: ['http', 'https', 'mailto', 'tel'],
autolink: true,
linkOnPaste: true,
})

export { CustomLinkExtension }

export type { CustomLinkExtensionOptions }
Loading