This commit is contained in:
Anton Tranelis 2025-06-04 21:41:44 +02:00
parent 3a319c6f09
commit 391530aba1
5 changed files with 206 additions and 231 deletions

34
package-lock.json generated
View File

@ -23,6 +23,7 @@
"leaflet.locatecontrol": "^0.79.0",
"radash": "^12.1.0",
"react-colorful": "^5.6.1",
"react-icons": "^5.5.0",
"react-image-crop": "^10.1.8",
"react-inlinesvg": "^4.2.0",
"react-leaflet": "^4.2.1",
@ -6318,9 +6319,9 @@
}
},
"node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
"integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -11114,6 +11115,15 @@
"react": "16.8 - 19"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-image-crop": {
"version": "10.1.8",
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-10.1.8.tgz",
@ -12543,13 +12553,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@ -13241,18 +13251,18 @@
}
},
"node_modules/vite": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.0.tgz",
"integrity": "sha512-9aC0n4pr6hIbvi1YOpFjwQ+QOTGssvbJKoeYkuHHGWwlXfdxQlI8L2qNMo9awEEcCPSiS+5mJZk5jH1PAqoDeQ==",
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.12"
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"

View File

@ -111,6 +111,7 @@
"leaflet.locatecontrol": "^0.79.0",
"radash": "^12.1.0",
"react-colorful": "^5.6.1",
"react-icons": "^5.5.0",
"react-image-crop": "^10.1.8",
"react-inlinesvg": "^4.2.0",
"react-leaflet": "^4.2.1",

View File

@ -1,210 +1,17 @@
import { Color } from '@tiptap/extension-color'
import Color from '@tiptap/extension-color'
import Image from '@tiptap/extension-image'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
import Youtube from '@tiptap/extension-youtube'
import { EditorProvider } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useState } from 'react'
import { Markdown } from 'tiptap-markdown'
import Youtube from '@tiptap/extension-youtube'
import { TextEditorMenu } from './TextEditorMenu'
import type { EditorEvents } from '@tiptap/react'
const MenuBar = () => {
const { editor } = useCurrentEditor()
if (!editor) {
return null
}
const [height, setHeight] = useState(480)
const [width, setWidth] = useState(640)
const addYoutubeVideo = () => {
const url = prompt('Enter YouTube URL')
if (url) {
editor.commands.setYoutubeVideo({
src: url,
width: Math.max(320, width) || 640,
height: Math.max(180, height) || 480,
})
}
}
return (
<div className='control-group tiptap-toolbar'>
<div className='button-group'>
<button
type='button'
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''}
>
Bold
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''}
>
Italic
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
className={editor.isActive('strike') ? 'is-active' : ''}
>
Strike
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
className={editor.isActive('code') ? 'is-active' : ''}
>
Code
</button>
<button type='button' onClick={() => editor.chain().focus().unsetAllMarks().run()}>
Clear marks
</button>
<button type='button' onClick={() => editor.chain().focus().clearNodes().run()}>
Clear nodes
</button>
<button
type='button'
onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive('paragraph') ? 'is-active' : ''}
>
Paragraph
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
>
H1
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
>
H2
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
>
H3
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
>
H4
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
>
H5
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
>
H6
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''}
>
Bullet list
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''}
>
Ordered list
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''}
>
Code block
</button>
<button
type='button'
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive('blockquote') ? 'is-active' : ''}
>
Blockquote
</button>
<button type='button' onClick={() => editor.chain().focus().setHorizontalRule().run()}>
Horizontal rule
</button>
<button type='button' onClick={() => editor.chain().focus().setHardBreak().run()}>
Hard break
</button>
<button
type='button'
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()}
>
Undo
</button>
<button
type='button'
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()}
>
Redo
</button>
<button
type='button'
onClick={() => editor.chain().focus().setColor('#958DF1').run()}
className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
>
Purple
</button>
</div>
<div className='button-group'>
<input
id='width'
type='number'
min='320'
max='1024'
placeholder='width'
value={width}
onChange={(event) => setWidth(parseInt(event.target.value))}
/>
<input
id='height'
type='number'
min='180'
max='720'
placeholder='height'
value={height}
onChange={(event) => setHeight(parseInt(event.target.value))}
/>
<button id='add' onClick={addYoutubeVideo}>
Add YouTube video
</button>
</div>
</div>
)
}
const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
// TextStyle.configure({ types: [ListItem.name] }),
@ -281,16 +88,30 @@ export function RichTextEditor({
}
return (
<EditorProvider
slotBefore={<MenuBar />}
extensions={extensions}
content={inputValue}
onUpdate={handleChange}
editorProps={{
attributes: {
class: 'prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none',
},
}}
></EditorProvider>
<div className={`tw:form-control tw:w-full ${containerStyle ?? ''}`}>
{labelTitle ? (
<label className='tw:label'>
<span className={`tw:label-text tw:text-base-content ${labelStyle ?? ''}`}>
{labelTitle}
</span>
</label>
) : null}
<EditorProvider
slotBefore={
<>
<br />
<TextEditorMenu />
</>
}
extensions={extensions}
content={inputValue}
onUpdate={handleChange}
editorProps={{
attributes: {
class: 'prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none',
},
}}
></EditorProvider>
</div>
)
}

View File

@ -0,0 +1,153 @@
import BoldIcon from '@heroicons/react/24/solid/BoldIcon'
import CodeBracketIcon from '@heroicons/react/24/solid/CodeBracketIcon'
import H1Icon from '@heroicons/react/24/solid/H1Icon'
import H2Icon from '@heroicons/react/24/solid/H2Icon'
import H3Icon from '@heroicons/react/24/solid/H3Icon'
import ItalicIcon from '@heroicons/react/24/solid/ItalicIcon'
import ListBulletIcon from '@heroicons/react/24/solid/ListBulletIcon'
import NumberedListIcon from '@heroicons/react/24/solid/NumberedListIcon'
import { useCurrentEditor } from '@tiptap/react'
import { FaQuoteLeft } from 'react-icons/fa6'
import { MdUndo, MdRedo, MdHorizontalRule } from 'react-icons/md'
export const TextEditorMenu = () => {
const { editor } = useCurrentEditor()
if (!editor) {
return null
}
const addYoutubeVideo = () => {
const url = prompt('Enter YouTube URL')
if (url) {
editor.commands.setYoutubeVideo({
src: url,
width: Math.max(320) || 640,
height: Math.max(180) || 480,
})
}
}
return (
<>
<ul className='tw:menu tw:menu-horizontal tw:bg-base-200 tw:rounded-box tw:mt-6'>
<li>
<a
className='tw:tooltip'
data-tip='Bold'
onClick={() => editor.chain().focus().toggleBold().run()}
>
<BoldIcon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Italic'
onClick={() => editor.chain().focus().toggleItalic().run()}
>
<ItalicIcon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
>
<H1Icon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
>
<H2Icon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
>
<H3Icon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleBulletList().run()}
>
<ListBulletIcon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleOrderedList().run()}
>
<NumberedListIcon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
>
<CodeBracketIcon className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().toggleBlockquote().run()}
>
<FaQuoteLeft className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().redo().run()}
>
<MdRedo className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().undo().run()}
>
<MdUndo className='tw:w-5 tw:h-5' />
</a>
</li>
<li>
<a
className='tw:tooltip'
data-tip='Details'
onClick={() => editor.chain().focus().setHorizontalRule().run()}
>
<MdHorizontalRule className='tw:w-5 tw:h-5' />
</a>
</li>
</ul>
{/** <div className='control-group tiptap-toolbar'>
<div className='button-group'>
<button id='add' className='btn btn-sm' onClick={addYoutubeVideo}>
Add YouTube video
</button>
</div>
</div> */}
</>
)
}

View File

@ -1,13 +1,3 @@
.tiptap-toolbar {
button {
border: 2px solid black;
padding: 6px;
width: 80x;
border-radius: 8px;
background-color: #C1BCAC;
}
}
.tiptap h1,
.tiptap h2,
.tiptap h3,