Snai3i-LandingPage-FormBuilder / frontend / src / components / Forms / BubbleMenuEditor.tsx
BubbleMenuEditor.tsx
Raw
import { useState } from 'react';
import {
  BubbleMenu,
  type Editor,
  EditorContent,
  useEditor,
  FloatingMenu,
} from '@tiptap/react';
import Typography from '@tiptap/extension-typography';
import Underline from '@tiptap/extension-underline';
import Placeholder from '@tiptap/extension-placeholder';
import StarterKit from '@tiptap/starter-kit';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  Redo2Icon,
  RemoveFormattingIcon,
  StrikethroughIcon,
  UnderlineIcon,
  Undo2Icon,
} from 'lucide-react';

import { Button } from '@/components/ui/formsButton';
import { Toggle } from '@/components/ui/toggle';

const Menu = ({ editor }: { editor: Editor }) => (
  <div className="flex gap-0.5 rounded-md bg-muted-foreground p-0.5">
    <Button
      variant="ghost"
      size="sm"
      onClick={() => editor.chain().focus().undo().run()}
      disabled={!editor.can().chain().focus().undo().run()}
      className="px-2 text-white hover:bg-white"
    >
      <Undo2Icon className="h-4 w-4" />
    </Button>
    <Button
      variant="ghost"
      size="sm"
      onClick={() => editor.chain().focus().redo().run()}
      disabled={!editor.can().chain().focus().redo().run()}
      className="px-2 text-white hover:bg-white"
    >
      <Redo2Icon className="h-4 w-4" />
    </Button>
    <Toggle
      size="sm"
      pressed={editor.isActive('bold')}
      onPressedChange={() => editor.chain().focus().toggleBold().run()}
      disabled={!editor.can().chain().focus().toggleBold().run()}
      className="text-white hover:bg-white data-[state=on]:bg-white"
    >
      <BoldIcon className="h-4 w-4" />
    </Toggle>
    <Toggle
      size="sm"
      pressed={editor.isActive('italic')}
      onPressedChange={() => editor.chain().focus().toggleItalic().run()}
      disabled={!editor.can().chain().focus().toggleItalic().run()}
      className="text-white hover:bg-white data-[state=on]:bg-white"
    >
      <ItalicIcon className="h-4 w-4" />
    </Toggle>
    <Toggle
      size="sm"
      pressed={editor.isActive('underline')}
      onPressedChange={() => editor.chain().focus().toggleUnderline().run()}
      disabled={!editor.can().chain().focus().toggleUnderline().run()}
      className="text-white hover:bg-white data-[state=on]:bg-white"
    >
      <UnderlineIcon className="h-4 w-4" />
    </Toggle>
    <Toggle
      size="sm"
      pressed={editor.isActive('strike')}
      onPressedChange={() => editor.chain().focus().toggleStrike().run()}
      disabled={!editor.can().chain().focus().toggleStrike().run()}
      className="text-white hover:bg-white data-[state=on]:bg-white"
    >
      <StrikethroughIcon className="h-4 w-4" />
    </Toggle>
    <Toggle
      size="sm"
      pressed={editor.isActive('code')}
      onPressedChange={() => editor.chain().focus().toggleCode().run()}
      disabled={!editor.can().chain().focus().toggleCode().run()}
      className="text-white hover:bg-white data-[state=on]:bg-white"
    >
      <Code2Icon className="h-4 w-4" />
    </Toggle>
    <Button
      variant="ghost"
      size="sm"
      onClick={() => editor.chain().focus().clearNodes().unsetAllMarks().run()}
      className="px-2 text-white hover:bg-white"
    >
      <RemoveFormattingIcon className="h-4 w-4" />
    </Button>
  </div>
);

interface Props {
  placeholder?: string;
  content?: string;
  updateHandler?: (html: string) => void;
  readOnly?: boolean;
  isHeading?: boolean;
}

export default function BubbleMenuEditor({
  placeholder,
  content,
  updateHandler,
  readOnly,
  isHeading,
}: Props) {
  const [isFocused, setIsFocused] = useState(false);

  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        heading: false,
        codeBlock: false,
        bulletList: false,
        orderedList: false,
        horizontalRule: false,
        blockquote: false,
        listItem: false,
        dropcursor: false,
        gapcursor: false,
        code: {
          HTMLAttributes: {
            class: 'bg-muted rounded p-1 box-decoration-clone',
          },
        },
        paragraph: {
          HTMLAttributes: {
            class: `font-medium pb-1 ${isHeading ? "text-xl" : "text-sm"}`,
          },
        },
      }),
      Underline,
      Placeholder.configure({ placeholder }),
      Typography,
    ],
    editorProps: {
      attributes: {
        // class: 'outline-none min-h-6 prose prose-slate max-w-none',
        class: 'outline-none prose prose-slate max-w-none',
      },
    },
    editable: !readOnly,
    onFocus: ({ editor, event }) => {
      if ('sourceCapabilities' in event && event.sourceCapabilities)
        editor.commands.selectAll();
      setIsFocused(true);
    },
    onBlur: () => setIsFocused(false),
    content,
    onUpdate: ({ editor }) => {
      if (updateHandler) updateHandler(editor.getHTML());
    },
  });

  if (!editor) return <div className="w-full" />;

  return (
    <>
      <BubbleMenu editor={editor}>
        <Menu editor={editor} />
      </BubbleMenu>

      <FloatingMenu editor={editor}>
        <Menu editor={editor} />
      </FloatingMenu>

      <EditorContent
        editor={editor}
        // className={`min-h-6 w-full transition-colors ${
        className={`w-full transition-colors ${
          isFocused ? 'border-primary' : 'hover:border-ring'
        } ${readOnly ? '' : 'border-b px-0.5 py-1'}`}
      />
    </>
  );
}