当前位置: 首页 > article >正文

用tiptap搭建仿google-docs工具栏

本文为开发开源项目的真实开发经历,感兴趣的可以来给我的项目点个star,谢谢啦~

具体博文介绍:
开源|Documind协同文档(接入deepseek-r1、支持实时聊天)Documind 🚀 一个支持实时聊天和接入 - 掘金

前言

由于展示的代码都较为简单,只对个别地方进行讲解,自行阅读或AI辅助阅读即可。

抽离简单组件

这个工具栏由多个工具小组件组成,我们可以将简单的部分抽离成公共组件ToolbarButton然后通过传入的配置项ToolbarButtonProps来激活这个公共组件。

import { cn } from "@/lib/utils";
import { LucideIcon } from "lucide-react";

interface ToolbarButtonProps {
  onClick?: () => void; //点击事件
  isActive?: boolean; //激活状态样式
  icon: LucideIcon; //具体icon
  title: string; //名称
}
export const ToolbarButton = ({
  onClick,
  isActive,
  icon: Icon,
  title,
}: ToolbarButtonProps) => {
  return (
    <div className="flex flex-col items-center justify-center">
      <button
        type="button"
        onClick={onClick}
        title={title}
        className={cn(
          "text-sm h-7 min-w-7 flex items-center justify-center rounded-sm hover:bg-neutral-200/80",
          isActive && "bg-neutral-200/80"
        )}
      >
        <Icon className="size-4" />
      </button>
    </div>
  );
};


编写配置项

onClick看不懂的话可以去看官方文档或者问AI

import {
  Undo2Icon,
  Redo2Icon,
  PrinterIcon,
  SpellCheckIcon,
  BoldIcon,
  ItalicIcon,
  UnderlineIcon,
  MessageSquareIcon,
  ListTodoIcon,
  RemoveFormattingIcon,
  type LucideIcon,
} from "lucide-react";
import { Editor } from '@tiptap/react';

// 定义section项的类型
export interface ToolbarItem {
  label: string;
  icon: LucideIcon;
  onClick: (editor?: Editor) => void;
  isActive?: boolean;
  title: string;
}

//传入的editor用于绑定事件
export const getToolbarSections = (editor?: Editor): ToolbarItem[][] => [
  [
    {
      label: "Undo",
      icon: Undo2Icon,
      onClick: () => editor?.chain().focus().undo().run(),
      isActive: false,
      title: "Undo",
    },
    {
      label: "Redo",
      icon: Redo2Icon,
      onClick: () => editor?.chain().focus().redo().run(),
      isActive: false,
      title: "Redo",
    },
    {
      label: "Print",
      icon: PrinterIcon,
      onClick: () => {
        window.print();
      },
      title: "Print",
    },
    {
      label: "Spell Check",
      icon: SpellCheckIcon,
      onClick: () => {
        const current = editor?.view.dom.getAttribute("spellcheck");
        editor?.view.dom.setAttribute(
          "spellcheck",
          current === "true" ? "false" : "true"
        );
      },
      title: "Spell Check",
    },
  ],
  [
    {
      label: "Bold",
      icon: BoldIcon,
      isActive: typeof editor?.isActive === 'function' ? editor.isActive("bold") : false,
      onClick: () => editor?.chain().focus().toggleBold().run(),
      title: "Bold",
    },
    {
      label: "Italic",
      icon: ItalicIcon,
      isActive: typeof editor?.isActive === 'function' ? editor.isActive("italic") : false,
      onClick: () => editor?.chain().focus().toggleItalic().run(),
      title: "Italic",
    },
    {
      label: "Underline",
      icon: UnderlineIcon,
      isActive: editor?.isActive("underline"),
      onClick: () => editor?.chain().focus().toggleUnderline().run(),
      title: "Underline",
    },
  ],
  [
    {
      label: "Comment",
      icon: MessageSquareIcon,
      onClick: () => {
        editor?.chain().focus().addPendingComment().run();
      },
      isActive: editor?.isActive("liveblocksCommentMark"),
      title: "Comment",
    },
    {
      label: "List Todo",
      icon: ListTodoIcon,
      onClick: () => {
        editor?.chain().focus().toggleTaskList().run();
      },
      isActive: editor?.isActive("taskList"),
      title: "List Todo",
    },
    {
      label: "Remove Formatting",
      icon: RemoveFormattingIcon,
      onClick: () => {
        editor?.chain().focus().unsetAllMarks().run();
      },
      title: "Remove Formatting",
    },
  ],
];

组装配置项和公共组件

import { useEditorStore } from "@/store/use-editor-store";
import { getToolbarSections } from "@/lib/useSections";
const { editor } = useEditorStore();//来自zustand
const sections = getToolbarSections(editor || undefined);//传入的editor用于cnClick事件
{sections[0].map((item) => (
   <ToolbarButton key={item.label} {...item} />
))}

{sections[2].map((item) => (
   <ToolbarButton key={item.label} {...item} />
))}

编写复杂组件

有些组件功能相对复杂,所以无法抽离成公共组件

FontFamilyButton组件

使用了shadcn中的DropdownMenu套件

用于设置字体的fontFamily

"use client";

import { ChevronDownIcon } from "lucide-react";
import { useEditorStore } from "@/store/use-editor-store";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

//font预设
const fonts = [
  { label: "Arial", value: "Arial" },
  { label: "Times New Roman", value: "Times New Roman" },
  { label: "Courier New", value: "Courier New" },
  { label: "Georgia", value: "Georgia" },
  { label: "Verdana", value: "Verdana" },
];

export const FontFamilyButton = () => {
  const { editor } = useEditorStore(); //zustand状态管理

  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            title="Select font family"
            type="button"
            className={cn(
              "h-7 w-[120px] shrink-0 flex items-center justify-between rounded-sm hover:bg-neutral-200/80 px-1.5 overflow-hidden text-sm"
            )}
          >
            <span className="truncate">
              {editor?.getAttributes("textStyle").fontFamily || "Arial"}
            </span>
            <ChevronDownIcon className="ml-2 size-4 shrink-0" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-1 flex flex-col gap-y-1">
          {fonts.map(({ label, value }) => (
            <button
              onClick={() => editor?.chain().focus().setFontFamily(value).run()}
              key={value}
              title="Select font family"
              type="button"
              className={cn(
                "w-full flex items-center gap-x-2 px-2 py-1 rounded-sm hover:bg-neutral-200/80",
                editor?.getAttributes("textStyle").fontFamily === value &&
                  "bg-neutral-200/80"
              )}
              style={{ fontFamily: value }}
            >
              <span className="text-sm">{label}</span>
            </button>
          ))}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};


HeadingLevelButton组件

使用了shadcn中的DropdownMenu套件

用于设置标题大小

"use client";

import { type Level } from "@tiptap/extension-heading";
import { ChevronDownIcon } from "lucide-react";
import { useEditorStore } from "@/store/use-editor-store";
import {
  DropdownMenuContent,
  DropdownMenuTrigger,
  DropdownMenu,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

export const HeadingLevelButton = () => {
  const { editor } = useEditorStore();
  const headings = [
    { label: "Normal text", value: 0, fontSize: "16px" },
    { label: "Heading 1", value: 1, fontSize: "32px" },
    { label: "Heading 2", value: 2, fontSize: "24px" },
    { label: "Heading 3", value: 3, fontSize: "20px" },
    { label: "Heading 4", value: 4, fontSize: "18px" },
    { label: "Heading 5", value: 5, fontSize: "16px" },
  ];

  const getCurrentHeading = () => {
    for (let level = 1; level <= 5; level++) {
      if (editor?.isActive(`heading`, { level })) {
        return `Heading ${level}`;
      }
    }
    return "Normal text";
  };

  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button className="h-7 w-[120px] flex shrink-0 items-center justify-center rounded-sm hover:bg-neutral-200/80 overflow-hidden text-sm">
            <span className="truncate">{getCurrentHeading()}</span>
            <ChevronDownIcon className="ml-2 size-4 shrink-0" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-1 flex flex-col gap-y-1">
          {headings.map(({ label, value, fontSize }) => (
            <button
              key={value}
              style={{ fontSize }}
              onClick={() => {
                if (value === 0) {
                  editor?.chain().focus().setParagraph().run();
                } else {
                  editor
                    ?.chain()
                    .focus()
                    .toggleHeading({ level: value as Level })
                    .run();
                }
              }}
              className={cn(
                "flex item-ccenter gap-x-2 px-2 py-1 rounded-sm hover:bg-neutral-200/80",
                (value === 0 && !editor?.isActive("heading")) ||
                  (editor?.isActive("heading", { level: value }) &&
                    "bg-neutral-200/80")
              )}
            >
              {label}
            </button>
          ))}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};

FontSizeButton组件
import { MinusIcon, PlusIcon } from "lucide-react";
import { useEditorStore } from "@/store/use-editor-store";
import { useState, useEffect } from "react";

export const FontSizeButton = () => {
  const { editor } = useEditorStore();
  // 获取当前字体大小(去除px单位)
  const currentFontSize = editor?.getAttributes("textStyle").fontSize
    ? editor?.getAttributes("textStyle").fontSize.replace("px", "")
    : "16";
  const [fontSize, setFontSize] = useState(currentFontSize);
  const [inputValue, setInputValue] = useState(currentFontSize);
  const [isEditing, setIsEditing] = useState(false);

  const updateFontSize = (newSize: string) => {
    const size = parseInt(newSize); // 将字符串转换为数字
    if (!isNaN(size) && size > 0) {
      //应用层更新
      editor?.chain().focus().setFontSize(`${size}px`).run();
      //UI层状态更新
      setFontSize(newSize);
      setInputValue(newSize);
      setIsEditing(false);
    }
  };

  //用于显示当前选中文本的字体大小
  useEffect(() => {
    const update = () => {
      const current = editor?.getAttributes("textStyle").fontSize || "16px";
      const newFontSize = current.replace("px", "");
      setFontSize(newFontSize);
      setInputValue(newFontSize);
      setIsEditing(false);
    };
    //订阅tiptap的selectionUpdate事件
    editor?.on("selectionUpdate", update);
    // 返回一个清理函数,用于在组件卸载时取消订阅
    return () => {
      editor?.off("selectionUpdate", update);
    };
  }, [editor]);

  // 在输入框输入内容时,更新输入框的值
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };

  // 在输入框失去焦点时,更新字体大小
  const handleInputBlur = () => {
    updateFontSize(inputValue);
  };

  // 在输入框按下回车键时,更新字体大小
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      e.preventDefault();
      updateFontSize(inputValue);
      editor?.commands.focus();
    }
  };

  //字号减
  const increment = () => {
    const newSize = parseInt(fontSize) + 1;
    updateFontSize(newSize.toString());
  };

  //字号加
  const decrement = () => {
    const newSize = parseInt(fontSize) - 1;
    updateFontSize(newSize.toString());
  };

  return (
    <div className="flex items-center gap-x-0.5">
      {/* 减号按钮 */}
      <button
        className="shrink-0 h-7 w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
        onClick={decrement}
        title="font size"
        type="button"
      >
        <MinusIcon className="size-4" />
      </button>
      {/* 输入框 */}
      {isEditing ? (
        <input
          type="text"
          value={inputValue}
          onChange={handleInputChange} //编辑中保存
          onBlur={handleInputBlur} //失去焦点后保存
          onKeyDown={handleKeyDown} //回车保存
          className="border border-neutral-400 text-center h-7 w-10 rounded-sm bg-transparent focus:outline-none focus:ring-0"
        />
      ) : (
        <button
          className="text-sm border border-neutral-400 text-center h-7 w-10 rounded-sm bg-transparent focus:outline-none focus:ring-0"
          onClick={() => {
            setIsEditing(true);
            setFontSize(currentFontSize);
          }}
          title="font size"
          type="button"
        >
          {currentFontSize}
        </button>
      )}
      {/* 加号按钮 */}
      <button
        className="shrink-0 h-7 w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
        onClick={increment}
        title="font size"
        type="button"
      >
        <PlusIcon className="size-4" />
      </button>
    </div>
  );
};


TextColorbutton组件

使用了shadcn中的DropdownMenu套件和react-color中的SketchPicker 颜色选择器

用于设置字体颜色

import { useEditorStore } from "@/store/use-editor-store";
import {
  DropdownMenuContent,
  DropdownMenuTrigger,
  DropdownMenu,
} from "@/components/ui/dropdown-menu";
import { type ColorResult, SketchPicker } from "react-color";
export const TextColorbutton = () => {
  const { editor } = useEditorStore();
  const value = editor?.getAttributes("textStyle").color || "#000000";//当前所选文本颜色
  
  //用于选择颜色后SketchPicker 组件会将其传入onChange
  const onChange = (color: ColorResult) => {
    editor?.chain().focus().setColor(color.hex).run();
  };
  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            title="Text Color"
            className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
          >
            <span className="text-xs">A</span>
            <div
              className="h-0.5 w-full"
              style={{ backgroundColor: value }}
            ></div>
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-0">
           <SketchPicker 
             color={value} // 传入当前颜色以展示
             onChange={onChange} //设置tiptap中文本颜色
           />
         </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};

HighlightButton组件

和上面TextColorbutton组件相似,这里是用于设置字体的背景颜色

import { useEditorStore } from "@/store/use-editor-store";
import {
  DropdownMenuContent,
  DropdownMenuTrigger,
  DropdownMenu,
} from "@/components/ui/dropdown-menu";
import { type ColorResult, SketchPicker } from "react-color";
import { HighlighterIcon } from "lucide-react";
export const HighlightButton = () => {
  const { editor } = useEditorStore();
  const value = editor?.getAttributes("highlight").color || "#FFFFFFFF";
  const onChange = (color: ColorResult) => {
    editor?.chain().focus().setHighlight({ color: color.hex }).run();
  };
  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            title="Text Color"
            className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
          >
            <HighlighterIcon className="size-4" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-0">
          <SketchPicker color={value} onChange={onChange} />
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};

LinkButton组件

使用了shadcn中的DropdownMenu套件

用于给选中文本添加跳转链接

import { useEditorStore } from "@/store/use-editor-store";
import { useState } from "react";
import { Link2Icon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
  DropdownMenuContent,
  DropdownMenuTrigger,
  DropdownMenu,
} from "@/components/ui/dropdown-menu";

export const LinkButton = () => {
  const { editor } = useEditorStore();
  const [value, setValue] = useState("");
  //给选中文本设置链接属性
  const onChange = (href: string) => {
    editor?.chain().focus().extendMarkRange("link").setLink({ href }).run();
    setValue("");
  };
  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu
        //下拉菜单时提取当前所选文本的链接属性
        onOpenChange={(open) => {
          if (open) {
            setValue(editor?.getAttributes("link").href || "");
          }
        }}
      >
        <DropdownMenuTrigger asChild>
          <button
            title="Text Color"
            className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
          >
            <Link2Icon className="size-4" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-2.5 flex items-center gap-x-2">
          <Input
            placeholder="https://example.com"
            value={value}
            onChange={(e) => setValue(e.target.value)}
          />
          {/* 点击后触发默认事件关闭下拉菜单 */}
          <Button onClick={() => onChange(value)}>Apply</Button>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};


ImageButton组件

使用了cloudinary的上传组件CldUploadButton

依赖下载:npm i next-cloudinary

需要在env环境变量中添加CLOUDINARY_URL=xxxx(只需在env中设置无需显示调用,在官网获取)uploadPreset的值也需要从cloudinary官网获取,具体见next-cloudinary文档。

此组件引出了闭包捕获问题,具体见文章:

import { useEditorStore } from "@/store/use-editor-store";
import { ImageIcon } from "lucide-react";
import { CldUploadButton } from "next-cloudinary";


export const ImageButton = () => {
  const onChange = (src: string) => {
    const currentEditor = useEditorStore.getState().editor; 
    currentEditor?.chain().focus().setImage({ src }).run();
  }

  const uploadPhoto = (result: any) => {
    onChange(result?.info?.secure_url);
  };

  return (
    <div className="flex flex-col items-center justify-center">
      {/* 图片插入下拉菜单 */}
      <CldUploadButton
        options={{ maxFiles: 1 }}
        onSuccess={uploadPhoto}
        uploadPreset="官网获取"
      >
        <ImageIcon className="size-4" />
      </CldUploadButton>
    </div>
  );
};
AlignButton组件

使用了shadcn中的DropdownMenu套件

用于提供四种文本对其方式:

  • 左对齐(AlignLeft)
  • 居中对齐(Align Center)
  • 右对齐(AlignRight)
  • 两端对齐(AlignJustify)
import {
  AlignCenterIcon,
  AlignJustifyIcon,
  AlignLeftIcon,
  AlignRightIcon,
} from "lucide-react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/use-editor-store";

export const AlignButton = () => {
  const { editor } = useEditorStore();
  const alignments = [
    {
      label: "Align Left",
      icon: AlignLeftIcon,
      value: "left",
    },
    {
      label: "Align Center",
      icon: AlignCenterIcon,
      value: "center",
    },
    {
      label: "Align Right",
      icon: AlignRightIcon,
      value: "right",
    },
    {
      label: "Align Justify",
      icon: AlignJustifyIcon,
      value: "justify",
    },
  ];
  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            title="Align"
            className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
          >
            <AlignLeftIcon className="size-4" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-0">
          {alignments.map(({ label, icon: Icon, value }) => (
            <button
              key={value}
              title={label}
              onClick={() => editor?.chain().focus().setTextAlign(value).run()}
              className={cn(
                "w-full flex items-center gap-x-2 px-1 py-1 rounded-sm hover:bg-neutral-200/80",
                editor?.isActive({ textAlign: value }) && "bg-neutral-200/80"
              )}
            >
              <Icon className="size-4" />
              <span className="text-sm">{label}</span>
            </button>
          ))}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};

ListButton组件

使用了shadcn中的DropdownMenu套件

用于提供下拉菜单切换无序列表和有序列表

import { ListIcon, ListOrderedIcon } from "lucide-react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/use-editor-store";

export const ListButton = () => {
  const { editor } = useEditorStore();
  const lists = [
    {
      label: "Bullet List",
      icon: ListIcon,
      isActive: () => editor?.isActive("bulletlist"),
      onClick: () => editor?.chain().focus().toggleBulletList().run(),
    },
    {
      label: "Ordered List",
      icon: ListOrderedIcon,
      isActive: () => editor?.isActive("orderedlist"),
      onClick: () => editor?.chain().focus().toggleOrderedList().run(),
    },
  ];
  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            title="Align"
            className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
            type="button"
          >
            <ListIcon className="size-4" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-0">
          {lists.map(({ label, icon: Icon, onClick, isActive }) => (
            <button
              key={label}
              onClick={onClick}
              className={cn(
                "w-full flex items-center gap-x-2 px-1 py-1 rounded-sm hover:bg-neutral-200/80",
                isActive() && "bg-neutral-200/80"
              )}
            >
              <Icon className="size-4" />
              <span className="text-sm">{label}</span>
            </button>
          ))}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};

LineHeightButton组件

使用了shadcn中的DropdownMenu套件

用于切换行高

import { ListCollapseIcon } from "lucide-react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/use-editor-store";

export const LineHeightButton = () => {
  const { editor } = useEditorStore();
  const listHeights = [
    {label:"Default",value:"normal"},
    {label:"Single",value:"1"},
    {label:"1.15",value:"1.15"},
    {label:"1.5",value:"1.5"},
    {label:"Double",value:"2"},
  ];
  return (
    <div className="flex flex-col items-center justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            title="Align"
            className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
          >
            <ListCollapseIcon className="size-4" />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent className="p-0">
          {listHeights.map(({ label, value }) => (
            <button
              key={value}
              title={label}
              onClick={() => editor?.chain().focus().setLineHeight(value).run()}
              type="button"
              className={cn(
                "w-full flex items-center gap-x-2 px-1 py-1 rounded-sm hover:bg-neutral-200/80" ,
                editor?.getAttributes("paragraph")?.lineHeight === value && "bg-neutral-200/80"
              )}
            >
              <span className="text-sm">{label}</span>
            </button>
          ))}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
};

拼接成toolbar工具栏组件

Separator 为shadcn中分割线组件

"use client";
import { ToolbarButton } from "./toolBarButton";
import { useEditorStore } from "@/store/use-editor-store";
import { Separator } from "@/components/ui/separator";
import { FontFamilyButton } from "./fontFamilyButton";
import { getToolbarSections } from "@/lib/useSections";
import { HeadingLevelButton } from "./headingButton";
import { TextColorbutton } from "./textColorButton";
import { HighlightButton } from "./highLightButton";
import { LinkButton } from "./linkButton";
import { ImageButton } from "./imageButton";
import { AlignButton } from "./alignButton";
import { ListButton } from "./ListButton";
import { FontSizeButton } from "./fontSizeButton";
import { LineHeightButton } from "./lineHeightButton";
export const Toolbar = () => {
  const { editor } = useEditorStore();
  const sections = getToolbarSections(editor || undefined);

  return (
    <div className="bg-[#F1F4F9] px-2.5 py-0.5 rounded-[24px] min-h-[40px] flex item-center gap-x-0.5 overflow-x-auto ">
      {sections[0].map((item) => (
        <ToolbarButton key={item.label} {...item} />
      ))}
      {/* 分隔符组件 */}
      <div className="flex flex-col items-center justify-center">
        <Separator orientation="vertical" className="h-6 bg-neutral-300" />
      </div>

      <FontFamilyButton />
      <div className="flex flex-col items-center justify-center">
        <Separator orientation="vertical" className="h-6 bg-neutral-300" />
      </div>
      <HeadingLevelButton />
      <div className="flex flex-col items-center justify-center">
        <Separator orientation="vertical" className="h-6 bg-neutral-300" />
      </div>
      {/* TODO:Font size */}
      <FontSizeButton />
      <div className="flex flex-col items-center justify-center">
        <Separator orientation="vertical" className="h-6 bg-neutral-300" />
      </div>
      {sections[1].map((item) => (
        <ToolbarButton key={item.label} {...item} />
      ))}

      <TextColorbutton />
      <HighlightButton />
      <div className="flex flex-col items-center justify-center">
        <Separator orientation="vertical" className="h-6 bg-neutral-300" />
      </div>
      <LinkButton />
      <ImageButton />
      <AlignButton />
      <ListButton />
      <LineHeightButton />
      {sections[2].map((item) => (
        <ToolbarButton key={item.label} {...item} />
      ))}
    </div>
  );
};


http://www.kler.cn/a/586619.html

相关文章:

  • JavaScript基础篇:五、 流程控制语句
  • java学习笔记2
  • 告别XML模板的繁琐!Word文档导出,easy!
  • Kubernetes 单节点集群搭建
  • tcpdump剖析:入门网络流量分析实战指南
  • Ubuntu从源代码编译安装QT
  • 进程间通信--匿名管道
  • 【蓝桥杯】雪地工程核弹引爆控制器最小数量计算
  • Pytorch实现之最小二乘梯度归一化设计
  • 在离线情况下如何使用 Python 翻译文本
  • 【JVM】性能监控与调优概述篇
  • 基于RAGFlow本地部署DeepSpeek-R1大模型与知识库:从配置到应用的全流程解析
  • Spring Boot 中 BootstrapRegistryInitializer 的作用与示例
  • Ubuntu中为curl和Docker配置代理
  • Docker安装GitLab中文版详细流程,以及非80端口配置
  • 2025年渗透测试面试题总结-安恒 (题目+回答)
  • U盘提示格式化的深度解析与应对策略
  • 【新能源汽车研发测试能力建设深度解析:设备、趋势与行业展望】
  • 学习用WinDbg查看程序当前运行的堆栈
  • [C语言日寄] qsort函数的练习