From f2c6687660f43e7ae15d3af2556715c0dfb2bf74 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 10 Oct 2024 22:44:56 +0200 Subject: [PATCH] feat: add buttons extension blocks --- .../Activities/DynamicCanva/DynamicCanva.tsx | 5 + apps/web/components/Objects/Editor/Editor.tsx | 5 + .../Editor/Extensions/Buttons/Buttons.ts | 43 +++++ .../Extensions/Buttons/ButtonsExtension.tsx | 168 ++++++++++++++++++ .../Objects/Editor/Toolbar/ToolbarButtons.tsx | 24 ++- 5 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 apps/web/components/Objects/Editor/Extensions/Buttons/Buttons.ts create mode 100644 apps/web/components/Objects/Editor/Extensions/Buttons/ButtonsExtension.tsx diff --git a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx index 998cf8db..86460612 100644 --- a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx +++ b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx @@ -26,6 +26,7 @@ import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext' import AICanvaToolkit from './AI/AICanvaToolkit' import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects' import Badges from '@components/Objects/Editor/Extensions/Badges/Badges' +import Buttons from '@components/Objects/Editor/Extensions/Buttons/Buttons' interface Editor { content: string @@ -95,6 +96,10 @@ function Canva(props: Editor) { editable: isEditable, activity: props.activity, }), + Buttons.configure({ + editable: isEditable, + activity: props.activity, + }), ], content: props.content, diff --git a/apps/web/components/Objects/Editor/Editor.tsx b/apps/web/components/Objects/Editor/Editor.tsx index f416d35a..b15a8b4f 100644 --- a/apps/web/components/Objects/Editor/Editor.tsx +++ b/apps/web/components/Objects/Editor/Editor.tsx @@ -50,6 +50,7 @@ import ActiveAvatars from './ActiveAvatars' import { getUriWithOrg } from '@services/config/config' import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects' import Badges from './Extensions/Badges/Badges' +import Buttons from './Extensions/Buttons/Buttons' interface Editor { content: string @@ -143,6 +144,10 @@ function Editor(props: Editor) { editable: true, activity: props.activity, }), + Buttons.configure({ + editable: true, + activity: props.activity, + }), // Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true ...(props.isCollabEnabledOnThisOrg ? [ diff --git a/apps/web/components/Objects/Editor/Extensions/Buttons/Buttons.ts b/apps/web/components/Objects/Editor/Extensions/Buttons/Buttons.ts new file mode 100644 index 00000000..298bbaa7 --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/Buttons/Buttons.ts @@ -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); + }, +}); \ No newline at end of file diff --git a/apps/web/components/Objects/Editor/Extensions/Buttons/ButtonsExtension.tsx b/apps/web/components/Objects/Editor/Extensions/Buttons/ButtonsExtension.tsx new file mode 100644 index 00000000..63c2e65a --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/Buttons/ButtonsExtension.tsx @@ -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(null) + const linkInputRef = useRef(null) + const colorPickerRef = useRef(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) => { + 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 ( + +
+ + {isEditable && ( +
+ + + + + + +
+ )} +
+ {isEditable && showEmojiPicker && ( +
+ +
+ )} + {isEditable && showLinkInput && ( + + )} + {isEditable && showColorPicker && ( +
+
+ {colors.map((c) => ( +
+
+ )} +
+ ) +} + +export default ButtonsExtension diff --git a/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx b/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx index f967e985..e96deef4 100644 --- a/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx +++ b/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx @@ -17,6 +17,7 @@ import { FileText, ImagePlus, Lightbulb, + MousePointerClick, Sigma, Tag, Tags, @@ -211,16 +212,31 @@ export const ToolbarButtons = ({ editor, props }: any) => { onClick={() => editor.chain().focus().insertContent({ type: 'badge', content: [ - { - type: 'text', - text: 'This is a Badge' - } + { + type: 'text', + text: 'This is a Badge' + } ] }).run()} > + + editor.chain().focus().insertContent({ + type: 'button', + content: [ + { + type: 'text', + text: 'Click me' + } + ] + }).run()} + > + + + ) }