Skip to content

Commit 73bd955

Browse files
committed
feat: editor support image and list
1 parent 62d2264 commit 73bd955

File tree

5 files changed

+173
-4
lines changed

5 files changed

+173
-4
lines changed

components/Tiptap.tsx

+68-4
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@ import Placeholder from '@tiptap/extension-placeholder'
55
import { Heading } from '@tiptap/extension-heading'
66
import { useLocalStorageValue } from "@react-hookz/web"
77
import { CREATE_CACHE } from "../constants"
8+
import { BulletList } from "@tiptap/extension-bullet-list"
9+
import { OrderedList } from "@tiptap/extension-ordered-list"
10+
import { ListItem } from "@tiptap/extension-list-item"
11+
import { Image } from "@tiptap/extension-image"
12+
import { UploadImageDialog } from "./UploadImageDialog"
13+
import { useState } from "react"
814

915
export const Tiptap = () => {
16+
const [openImageDialog, setOpenImageDialog] = useState(false)
1017
const [, setCache] = useLocalStorageValue<any>(CREATE_CACHE)
1118
const editor = useEditor({
1219
onUpdate: ({ editor : e }) => {
1320
const json = e.getJSON()
1421
setCache(json)
1522
},
1623
extensions: [
24+
BulletList,
25+
OrderedList,
26+
ListItem,
27+
Image,
1728
StarterKit,
1829
Paragraph.configure({
1930
HTMLAttributes: {
@@ -30,7 +41,7 @@ export const Tiptap = () => {
3041
},
3142
}),
3243
Heading.configure({
33-
levels: [1, 2, 3],
44+
levels: [1, 2],
3445
}),
3546
],
3647
content: '',
@@ -42,19 +53,64 @@ export const Tiptap = () => {
4253
},
4354
})
4455

56+
const handleImage = (url) => {
57+
if (url) {
58+
editor.chain().focus().setImage({ src: url }).run()
59+
}
60+
setOpenImageDialog(false)
61+
}
62+
4563
return (
46-
<>
64+
<div>
4765
{editor && <BubbleMenu editor={editor} pluginKey={'menu'}>
4866
<MenuUI editor={editor} />
4967
</BubbleMenu>}
68+
<UploadImageDialog open={openImageDialog} close={() => setOpenImageDialog(false)} save={handleImage} />
5069
<EditorContent editor={editor} />
51-
</>
70+
<div className='fixed bottom-6 inset-x-0'>
71+
<div className="flex justify-center">
72+
<FixedMenuUI editor={editor} openImageDialog={() => setOpenImageDialog(true)} />
73+
</div>
74+
</div>
75+
</div>
76+
)
77+
}
78+
79+
const FixedMenuUI = ({ editor, openImageDialog }: {editor: Editor, openImageDialog: () => void}) => {
80+
if (!editor) return null
81+
return (
82+
<div className=" justify-center bg-gray-100 rounded flex px-2 divide-x w-80">
83+
<div className="p-2 text-gray-800">
84+
<button
85+
onClick={(e) => { editor.chain().focus().toggleBulletList().run(); e.preventDefault() }}
86+
className={editor.isActive('bulletList') ? 'is-active' : ''}
87+
>
88+
Bullet list
89+
</button>
90+
</div>
91+
<div className="p-2 text-gray-800">
92+
<button
93+
onClick={(e) => {editor.chain().focus().toggleOrderedList().run(); e.preventDefault()}}
94+
className={editor.isActive('orderedList') ? 'is-active' : ''}
95+
>
96+
Order list
97+
</button>
98+
</div>
99+
<div className="p-2 text-gray-800">
100+
<button
101+
onClick={(e) => { e.preventDefault(); openImageDialog()}}
102+
>
103+
Image
104+
</button>
105+
</div>
106+
</div>
52107
)
53108
}
54109

55110
const MenuUI = ({ editor }: {editor: Editor}) => {
111+
if (!editor) return null
56112
return (
57-
<div className="shadow-md bg-white rounded flex px-2">
113+
<div className="shadow-md bg-gray-100 rounded flex px-2 divide-x">
58114
<div className="p-2 text-gray-800">
59115
<button
60116
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
@@ -63,6 +119,14 @@ const MenuUI = ({ editor }: {editor: Editor}) => {
63119
h1
64120
</button>
65121
</div>
122+
<div className="p-2 text-gray-800">
123+
<button
124+
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
125+
className={editor.isActive('h-1') ? 'is-active' : ''}
126+
>
127+
h2
128+
</button>
129+
</div>
66130
<div className="p-2 text-gray-800">
67131
<button
68132
onClick={() => editor.chain().focus().toggleBold().run()}

components/UploadImageDialog.tsx

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Fragment, useState } from 'react'
2+
import { Dialog, Transition } from '@headlessui/react'
3+
4+
interface UploadImageDialogProps {
5+
open: boolean
6+
close(): void
7+
save(url: string): void
8+
}
9+
10+
export function UploadImageDialog({ open, close, save }: UploadImageDialogProps) {
11+
const [url, setUrl] = useState<string>()
12+
13+
return (
14+
<>
15+
<Transition
16+
show={open}
17+
enter="transition duration-100 ease-out"
18+
enterFrom="transform scale-95 opacity-0"
19+
enterTo="transform scale-100 opacity-100"
20+
leave="transition duration-75 ease-out"
21+
leaveFrom="transform scale-100 opacity-100"
22+
leaveTo="transform scale-95 opacity-0"
23+
>
24+
<Dialog
25+
as="div"
26+
className="fixed inset-0 z-10 overflow-y-auto"
27+
onClose={close}
28+
>
29+
<div className="min-h-screen px-4 text-center">
30+
<Transition.Child
31+
as={Fragment}
32+
enter="ease-out duration-300"
33+
enterFrom="opacity-0"
34+
enterTo="opacity-100"
35+
leave="ease-in duration-200"
36+
leaveFrom="opacity-100"
37+
leaveTo="opacity-0"
38+
>
39+
<Dialog.Overlay className="fixed inset-0 bg-grey-100" />
40+
</Transition.Child>
41+
42+
{/* This element is to trick the browser into centering the modal contents. */}
43+
<span
44+
className="inline-block h-screen align-middle"
45+
aria-hidden="true"
46+
>
47+
&#8203;
48+
</span>
49+
<Transition.Child
50+
as={Fragment}
51+
enter="ease-out duration-300"
52+
enterFrom="opacity-0 scale-95"
53+
enterTo="opacity-100 scale-100"
54+
leave="ease-in duration-200"
55+
leaveFrom="opacity-100 scale-100"
56+
leaveTo="opacity-0 scale-95"
57+
>
58+
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
59+
<Dialog.Title
60+
as="h3"
61+
className="text-lg font-medium leading-6 text-gray-900"
62+
>
63+
Provider a Image URL
64+
</Dialog.Title>
65+
<div className="mt-2">
66+
<input
67+
onChange={e => setUrl(e.target.value)}
68+
type='text'
69+
placeholder="Image URL"
70+
className="w-full bg-white rounded border border-gray-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
71+
/>
72+
</div>
73+
<div className="mt-4">
74+
<button
75+
type="button"
76+
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
77+
onClick={() => save(url)}
78+
>
79+
Save
80+
</button>
81+
</div>
82+
</div>
83+
</Transition.Child>
84+
</div>
85+
</Dialog>
86+
</Transition>
87+
</>
88+
)
89+
}

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
"@react-hookz/web": "^12.0.4",
1717
"@textile/eth-storage": "^1.0.0",
1818
"@tiptap/extension-bubble-menu": "^2.0.0-beta.54",
19+
"@tiptap/extension-bullet-list": "^2.0.0-beta.26",
1920
"@tiptap/extension-heading": "^2.0.0-beta.25",
21+
"@tiptap/extension-image": "^2.0.0-beta.24",
22+
"@tiptap/extension-list-item": "^2.0.0-beta.20",
2023
"@tiptap/extension-paragraph": "^2.0.0-beta.23",
2124
"@tiptap/extension-placeholder": "^2.0.0-beta.46",
2225
"@tiptap/react": "^2.0.0-beta.104",

styles/globals.css

+12
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,16 @@ h3 {
2727
}
2828
body {
2929
@apply text-gray-800;
30+
}
31+
32+
ul, ol {
33+
padding: 0 1rem;
34+
}
35+
36+
ul {
37+
list-style: disc;
38+
}
39+
40+
ol {
41+
list-style: decimal;
3042
}

tailwind.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
2+
mode: 'jit',
23
purge: {
34
content: ["./pages/*.js", "./pages/*.tsx", "./components/*.tsx"],
45
},

0 commit comments

Comments
 (0)