feat: add buttons extension blocks

This commit is contained in:
swve 2024-10-10 22:44:56 +02:00
parent 8965ee67c3
commit f2c6687660
5 changed files with 241 additions and 4 deletions

View file

@ -26,6 +26,7 @@ import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
import AICanvaToolkit from './AI/AICanvaToolkit' import AICanvaToolkit from './AI/AICanvaToolkit'
import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects' import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects'
import Badges from '@components/Objects/Editor/Extensions/Badges/Badges' import Badges from '@components/Objects/Editor/Extensions/Badges/Badges'
import Buttons from '@components/Objects/Editor/Extensions/Buttons/Buttons'
interface Editor { interface Editor {
content: string content: string
@ -95,6 +96,10 @@ function Canva(props: Editor) {
editable: isEditable, editable: isEditable,
activity: props.activity, activity: props.activity,
}), }),
Buttons.configure({
editable: isEditable,
activity: props.activity,
}),
], ],
content: props.content, content: props.content,

View file

@ -50,6 +50,7 @@ import ActiveAvatars from './ActiveAvatars'
import { getUriWithOrg } from '@services/config/config' import { getUriWithOrg } from '@services/config/config'
import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects' import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects'
import Badges from './Extensions/Badges/Badges' import Badges from './Extensions/Badges/Badges'
import Buttons from './Extensions/Buttons/Buttons'
interface Editor { interface Editor {
content: string content: string
@ -143,6 +144,10 @@ function Editor(props: Editor) {
editable: true, editable: true,
activity: props.activity, activity: props.activity,
}), }),
Buttons.configure({
editable: true,
activity: props.activity,
}),
// Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true // Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true
...(props.isCollabEnabledOnThisOrg ? [ ...(props.isCollabEnabledOnThisOrg ? [

View file

@ -0,0 +1,43 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from "@tiptap/core";
import ButtonsExtension from "./ButtonsExtension";
export default Node.create({
name: "button",
group: "block",
draggable: true,
content: "text*",
addAttributes() {
return {
emoji: {
default: '🔗',
},
link: {
default: '',
},
color: {
default: 'blue',
},
alignment: {
default: 'left',
},
};
},
parseHTML() {
return [
{
tag: "button-block",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["button-block", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return ReactNodeViewRenderer(ButtonsExtension);
},
});

View file

@ -0,0 +1,168 @@
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import React, { useState, useRef, useEffect } from 'react'
import Picker from '@emoji-mart/react'
import { ArrowRight, ChevronDown, Link, AlignLeft, AlignCenter, AlignRight, Palette } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
const ButtonsExtension: React.FC = (props: any) => {
const [emoji, setEmoji] = useState(props.node.attrs.emoji)
const [link, setLink] = useState(props.node.attrs.link)
const [alignment, setAlignment] = useState(props.node.attrs.alignment)
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [showLinkInput, setShowLinkInput] = useState(false)
const [color, setColor] = useState(props.node.attrs.color || 'blue')
const [showColorPicker, setShowColorPicker] = useState(false)
const pickerRef = useRef<HTMLDivElement>(null)
const linkInputRef = useRef<HTMLInputElement>(null)
const colorPickerRef = useRef<HTMLDivElement>(null)
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
setShowEmojiPicker(false)
}
if (linkInputRef.current && !linkInputRef.current.contains(event.target as Node)) {
setShowLinkInput(false)
}
if (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) {
setShowColorPicker(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const handleEmojiSelect = (emoji: any) => {
setEmoji(emoji.native)
setShowEmojiPicker(false)
props.updateAttributes({
emoji: emoji.native,
})
}
const handleLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLink(e.target.value)
props.updateAttributes({
link: e.target.value,
})
}
const handleAlignmentChange = (newAlignment: 'left' | 'center' | 'right') => {
setAlignment(newAlignment)
props.updateAttributes({
alignment: newAlignment,
})
}
const getAlignmentClass = () => {
switch (alignment) {
case 'left': return 'text-left';
case 'center': return 'text-center';
case 'right': return 'text-right';
default: return 'text-left';
}
}
const handleColorSelect = (selectedColor: string) => {
setColor(selectedColor)
setShowColorPicker(false)
props.updateAttributes({
color: selectedColor,
})
}
const getButtonColor = (color: string) => {
switch (color) {
case 'sky': return 'bg-sky-500 hover:bg-sky-600';
case 'green': return 'bg-green-500 hover:bg-green-600';
case 'yellow': return 'bg-yellow-500 hover:bg-yellow-600';
case 'red': return 'bg-red-500 hover:bg-red-600';
case 'purple': return 'bg-purple-500 hover:bg-purple-600';
case 'teal': return 'bg-teal-500 hover:bg-teal-600';
case 'amber': return 'bg-amber-500 hover:bg-amber-600';
case 'indigo': return 'bg-indigo-500 hover:bg-indigo-600';
case 'neutral': return 'bg-neutral-500 hover:bg-neutral-600';
default: return 'bg-blue-500 hover:bg-blue-600';
}
}
const colors = ['sky', 'green', 'yellow', 'red', 'purple', 'teal', 'amber', 'indigo', 'neutral', 'blue']
return (
<NodeViewWrapper className={`block-button ${getAlignmentClass()}`}>
<div className='inline-block'>
<button
onClick={isEditable ? undefined : () => window.open(link, '_blank')}
className={twMerge(
'flex items-center space-x-2 py-2 px-4 rounded-xl text-white transition-colors',
getButtonColor(color),
isEditable && 'pointer-events-none',
!link && 'opacity-60'
)}
>
<span>{emoji}</span>
<NodeViewContent className="content" />
<ArrowRight size={14} />
</button>
{isEditable && (
<div className="flex mt-2 space-x-2">
<button onClick={() => setShowEmojiPicker(!showEmojiPicker)} className="p-1 bg-gray-200 rounded-md">
<ChevronDown size={14} />
</button>
<button onClick={() => setShowLinkInput(!showLinkInput)} className="p-1 bg-gray-200 rounded-md">
<Link size={14} />
</button>
<button onClick={() => handleAlignmentChange('left')} className="p-1 bg-gray-200 rounded-md">
<AlignLeft size={14} />
</button>
<button onClick={() => handleAlignmentChange('center')} className="p-1 bg-gray-200 rounded-md">
<AlignCenter size={14} />
</button>
<button onClick={() => handleAlignmentChange('right')} className="p-1 bg-gray-200 rounded-md">
<AlignRight size={14} />
</button>
<button onClick={() => setShowColorPicker(!showColorPicker)} className="p-1 bg-gray-200 rounded-md">
<Palette size={14} />
</button>
</div>
)}
</div>
{isEditable && showEmojiPicker && (
<div ref={pickerRef}>
<Picker onEmojiSelect={handleEmojiSelect} />
</div>
)}
{isEditable && showLinkInput && (
<input
ref={linkInputRef}
type="text"
value={link}
onChange={handleLinkChange}
placeholder="Enter link URL"
className="mt-2 p-2 w-full border rounded-md"
/>
)}
{isEditable && showColorPicker && (
<div ref={colorPickerRef} className="absolute mt-2 p-2 bg-white rounded-md nice-shadow">
<div className="flex flex-wrap gap-2">
{colors.map((c) => (
<button
key={c}
className={`w-6 h-6 rounded-full ${getButtonColor(c)} hover:ring-2 hover:ring-opacity-50 focus:outline-none focus:ring-2 focus:ring-opacity-50`}
onClick={() => handleColorSelect(c)}
/>
))}
</div>
</div>
)}
</NodeViewWrapper>
)
}
export default ButtonsExtension

View file

@ -17,6 +17,7 @@ import {
FileText, FileText,
ImagePlus, ImagePlus,
Lightbulb, Lightbulb,
MousePointerClick,
Sigma, Sigma,
Tag, Tag,
Tags, Tags,
@ -211,16 +212,31 @@ export const ToolbarButtons = ({ editor, props }: any) => {
onClick={() => editor.chain().focus().insertContent({ onClick={() => editor.chain().focus().insertContent({
type: 'badge', type: 'badge',
content: [ content: [
{ {
type: 'text', type: 'text',
text: 'This is a Badge' text: 'This is a Badge'
} }
] ]
}).run()} }).run()}
> >
<Tags size={15} /> <Tags size={15} />
</ToolBtn> </ToolBtn>
</ToolTip> </ToolTip>
<ToolTip content={'Button'}>
<ToolBtn
onClick={() => editor.chain().focus().insertContent({
type: 'button',
content: [
{
type: 'text',
text: 'Click me'
}
]
}).run()}
>
<MousePointerClick size={15} />
</ToolBtn>
</ToolTip>
</ToolButtonsWrapper> </ToolButtonsWrapper>
) )
} }