mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add buttons extension blocks
This commit is contained in:
parent
8965ee67c3
commit
f2c6687660
5 changed files with 241 additions and 4 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 ? [
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
FileText,
|
FileText,
|
||||||
ImagePlus,
|
ImagePlus,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
|
MousePointerClick,
|
||||||
Sigma,
|
Sigma,
|
||||||
Tag,
|
Tag,
|
||||||
Tags,
|
Tags,
|
||||||
|
|
@ -221,6 +222,21 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue