Skip to content

Commit

Permalink
feat: add shadcn-minimal-tiptap
Browse files Browse the repository at this point in the history
  • Loading branch information
TinsFox committed Dec 3, 2024
1 parent 13407ce commit ad5038f
Show file tree
Hide file tree
Showing 66 changed files with 4,819 additions and 142 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@
"hyoban",
"lucide",
"ofetch",
"openapi",
"sonner",
"tanstack",
"tiptap",
"tsup"
],
"typescript.preferences.autoImportFileExcludePatterns": ["react-day-picker"],
Expand Down
2 changes: 2 additions & 0 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-icons": "^1.3.2",
"@repo/pro-table": "workspace:*",
"@repo/tiptap": "workspace:*",
"@repo/ui": "workspace:*",
"@tanstack/react-query": "^5.59.17",
"@tanstack/react-table": "^8.20.5",
"@tiptap/react": "^2.10.3",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"dotenv": "^16.4.5",
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "@repo/ui/globals.css"
import "@repo/tiptap/tiptap.css"
import "./i18n"

import { env } from "@env"
Expand Down
21 changes: 21 additions & 0 deletions apps/admin/src/pages/(external)/playground/tiptap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MinimalTiptapEditor } from "@repo/tiptap/minimal-tiptap"
import type { Content } from "@tiptap/react"
import { useState } from "react"

export function Component() {
const [value, setValue] = useState<Content>("")

return (
<MinimalTiptapEditor
value={value}
onChange={setValue}
className="w-full"
editorContentClassName="p-5"
output="html"
placeholder="Type your description here..."
autofocus={true}
editable={true}
editorClassName="focus:outline-none"
/>
)
}
7 changes: 7 additions & 0 deletions packages/tiptap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# shadcn-minimal-tiptap

This package is a secondary development based on [shadcn-minimal-tiptap](https://github.com/Aslam97/shadcn-minimal-tiptap). It extends and enhances the functionality of the original project while maintaining its core features.

## About Original Project

The original [shadcn-minimal-tiptap](https://github.com/Aslam97/shadcn-minimal-tiptap) is a minimal implementation of Tiptap editor with shadcn/ui components.
39 changes: 39 additions & 0 deletions packages/tiptap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@repo/tiptap",
"version": "1.0.0",
"description": "",
"author": "",
"license": "ISC",
"keywords": [],
"exports": {
"./minimal-tiptap": "./src/minimal-tiptap.tsx",
"./minimal-tiptap-one": "./src/minimal-tiptap-one.tsx",
"./minimal-tiptap-three": "./src/minimal-tiptap-three.tsx",
"./tiptap.css": "./src/styles/index.css"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@repo/ui": "workspace:*",
"@tiptap/extension-code-block-lowlight": "^2.10.3",
"@tiptap/extension-color": "^2.10.3",
"@tiptap/extension-heading": "^2.10.3",
"@tiptap/extension-horizontal-rule": "^2.10.3",
"@tiptap/extension-image": "^2.10.3",
"@tiptap/extension-link": "^2.10.3",
"@tiptap/extension-placeholder": "^2.10.3",
"@tiptap/extension-text-style": "^2.10.3",
"@tiptap/extension-typography": "^2.10.3",
"@tiptap/extension-underline": "^2.10.3",
"@tiptap/pm": "^2.10.3",
"@tiptap/react": "^2.10.3",
"@tiptap/starter-kit": "^2.10.3",
"@types/react": "^18.3.12",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lowlight": "^3.2.0",
"react": "^18.3.1",
"react-medium-image-zoom": "^5.2.11",
"sonner": "^1.6.1",
"tailwind-merge": "^2.5.4"
}
}
107 changes: 107 additions & 0 deletions packages/tiptap/src/components/bubble-menu/link-bubble-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Editor } from "@tiptap/react"
import { BubbleMenu } from "@tiptap/react"
import * as React from "react"

import type { ShouldShowProps } from "../../types"
import { LinkEditBlock } from "../link/link-edit-block"
import { LinkPopoverBlock } from "../link/link-popover-block"

interface LinkBubbleMenuProps {
editor: Editor
}

interface LinkAttributes {
href: string
target: string
}

export const LinkBubbleMenu: React.FC<LinkBubbleMenuProps> = ({ editor }) => {
const [showEdit, setShowEdit] = React.useState(false)
const [linkAttrs, setLinkAttrs] = React.useState<LinkAttributes>({ href: "", target: "" })
const [selectedText, setSelectedText] = React.useState("")

const updateLinkState = React.useCallback(() => {
const { from, to } = editor.state.selection
const { href, target } = editor.getAttributes("link")
const text = editor.state.doc.textBetween(from, to, " ")

setLinkAttrs({ href, target })
setSelectedText(text)
}, [editor])

const shouldShow = React.useCallback(
({ editor, from, to }: ShouldShowProps) => {
if (from === to) {
return false
}
const { href } = editor.getAttributes("link")

if (href) {
updateLinkState()
return true
}
return false
},
[updateLinkState],
)

const handleEdit = React.useCallback(() => {
setShowEdit(true)
}, [])

const onSetLink = React.useCallback(
(url: string, text?: string, openInNewTab?: boolean) => {
editor
.chain()
.focus()
.extendMarkRange("link")
.insertContent({
type: "text",
text: text || url,
marks: [
{
type: "link",
attrs: {
href: url,
target: openInNewTab ? "_blank" : "",
},
},
],
})
.setLink({ href: url, target: openInNewTab ? "_blank" : "" })
.run()
setShowEdit(false)
updateLinkState()
},
[editor, updateLinkState],
)

const onUnsetLink = React.useCallback(() => {
editor.chain().focus().extendMarkRange("link").unsetLink().run()
setShowEdit(false)
updateLinkState()
}, [editor, updateLinkState])

return (
<BubbleMenu
editor={editor}
shouldShow={shouldShow}
tippyOptions={{
placement: "bottom-start",
onHidden: () => setShowEdit(false),
}}
>
{showEdit ? (
<LinkEditBlock
defaultUrl={linkAttrs.href}
defaultText={selectedText}
defaultIsNewTab={linkAttrs.target === "_blank"}
onSave={onSetLink}
className="w-full min-w-80 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none"
/>
) : (
<LinkPopoverBlock onClear={onUnsetLink} url={linkAttrs.href} onEdit={handleEdit} />
)}
</BubbleMenu>
)
}
83 changes: 83 additions & 0 deletions packages/tiptap/src/components/image/image-edit-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Editor } from "@tiptap/react"
import * as React from "react"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

interface ImageEditBlockProps {
editor: Editor
close: () => void
}

export const ImageEditBlock: React.FC<ImageEditBlockProps> = ({ editor, close }) => {
const fileInputRef = React.useRef<HTMLInputElement>(null)
const [link, setLink] = React.useState("")

const handleClick = React.useCallback(() => {
fileInputRef.current?.click()
}, [])

const handleFile = React.useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const { files } = e.target
if (!files?.length) return

const insertImages = async () => {
const contentBucket = []
const filesArray = Array.from(files)

for (const file of filesArray) {
contentBucket.push({ src: file })
}

editor.commands.setImages(contentBucket)
}

await insertImages()
close()
},
[editor, close],
)

const handleSubmit = React.useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
e.stopPropagation()

if (link) {
editor.commands.setImages([{ src: link }])
close()
}
},
[editor, link, close],
)

return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-1">
<Label htmlFor="image-link">Attach an image link</Label>
<div className="flex">
<Input
id="image-link"
type="url"
required
placeholder="https://example.com"
value={link}
className="grow"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setLink(e.target.value)}
/>
<Button type="submit" className="ml-2">
Submit
</Button>
</div>
</div>
<Button type="button" className="w-full" onClick={handleClick}>
Upload from your computer
</Button>
<input type="file" accept="image/*" ref={fileInputRef} multiple className="hidden" onChange={handleFile} />
</form>
)
}

export default ImageEditBlock
50 changes: 50 additions & 0 deletions packages/tiptap/src/components/image/image-edit-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ImageIcon } from "@radix-ui/react-icons"
import type { Editor } from "@tiptap/react"
import type { VariantProps } from "class-variance-authority"
import { useState } from "react"

import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import type { toggleVariants } from "@/components/ui/toggle"

import { ToolbarButton } from "../toolbar-button"
import { ImageEditBlock } from "./image-edit-block"

interface ImageEditDialogProps extends VariantProps<typeof toggleVariants> {
editor: Editor
}

const ImageEditDialog = ({ editor, size, variant }: ImageEditDialogProps) => {
const [open, setOpen] = useState(false)

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<ToolbarButton
isActive={editor.isActive("image")}
tooltip="Image"
aria-label="Image"
size={size}
variant={variant}
>
<ImageIcon className="size-5" />
</ToolbarButton>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Select image</DialogTitle>
<DialogDescription className="sr-only">Upload an image from your computer</DialogDescription>
</DialogHeader>
<ImageEditBlock editor={editor} close={() => setOpen(false)} />
</DialogContent>
</Dialog>
)
}

export { ImageEditDialog }
Loading

0 comments on commit ad5038f

Please sign in to comment.