iptap搭建仿google-docs工具栏

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

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

前言

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

抽离简单组件

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

typescript 复制代码
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

typescript 复制代码
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",
    },
  ],
];

组装配置项和公共组件

javascript 复制代码
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

typescript 复制代码
"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套件

用于设置标题大小

javascript 复制代码
"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组件

typescript 复制代码
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 颜色选择器

用于设置字体颜色

javascript 复制代码
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组件相似,这里是用于设置字体的背景颜色

typescript 复制代码
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套件

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

typescript 复制代码
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文档。

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

typescript 复制代码
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)
typescript 复制代码
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套件

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

typescript 复制代码
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套件

用于切换行高

typescript 复制代码
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中分割线组件

javascript 复制代码
"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>
  );
};
相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax