Fabric.js 按位置保存每个字母的样式:第 0 行第 6 个字母是粗体。代码修改 text 时,这些位置不会移动,所以插入文字之后,原来的粗体单词就不再是粗体了(fabric.js #6133)。setSelectionStyles 可以修改选中文字的样式,但文字本身改变时它帮不上忙。insertChars 和 removeChars 会移动样式,但它们不触发事件,用户输入时还会让隐藏的 textarea 落后。
试一试
正在加载在线示例,代码在下方。
"use client";
import { IText } from "fabric";
import { createTextCommands } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import {
BoldIcon,
RotateCcwIcon,
TextCursorIcon,
Undo2Icon,
WandIcon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { DemoCanvas, DemoFrame, Status, Toolbar, ToolButton } from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
const STRONG = { fontWeight: "bold", fill: "#6f00ff" } as const;
function makeText() {
return new IText("Hello world", {
left: 40,
top: 110,
originX: "left",
originY: "top",
fontSize: 46,
fill: "#0d0d0d",
styles: { 0: { 6: STRONG, 7: STRONG, 8: STRONG, 9: STRONG, 10: STRONG } },
});
}
export default function TextCommandsDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const textRef = useRef<IText | null>(null);
const [message, setMessage] = useState("");
function reset() {
if (!engine) return;
if (textRef.current) engine.canvas.remove(textRef.current);
const text = makeText();
textRef.current = text;
engine.canvas.add(text);
engine.clearHistory();
engine.canvas.requestRenderAll();
setMessage("");
}
useEffect(reset, [engine]);
function current() {
return engine?.getObjectById(
(textRef.current as unknown as { id: string } | null)?.id ?? ""
) as IText | undefined;
}
function insertWithCommands() {
const text = current();
if (!engine || !text) return;
createTextCommands(engine).insertText(text, 0, "Oh, ");
setMessage(t("textCommands.inserted"));
}
function setDirectly() {
const text = current();
if (!engine || !text) return;
text.set("text", `Oh, ${text.text}`);
engine.canvas.requestRenderAll();
setMessage(t("textCommands.direct"));
}
function boldFirstWord() {
const text = current();
if (!engine || !text) return;
const end = [...text.text].indexOf(" ");
createTextCommands(engine).setTextStyle(
text,
0,
end === -1 ? text.text.length : end,
{ fontWeight: "bold" }
);
setMessage(t("textCommands.styled"));
}
async function undo() {
if (!engine) return;
await engine.undo();
textRef.current = engine.canvas.getObjects()[0] as IText;
setMessage(t("textCommands.undone"));
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={WandIcon}
label={t("textCommands.insert")}
primary
disabled={!engine}
onClick={insertWithCommands}
/>
<ToolButton
icon={TextCursorIcon}
label={t("textCommands.setText")}
disabled={!engine}
onClick={setDirectly}
/>
<ToolButton
icon={BoldIcon}
label={t("textCommands.bold")}
disabled={!engine}
onClick={boldFirstWord}
/>
<ToolButton
icon={Undo2Icon}
label={t("undo")}
iconOnly
disabled={!engine}
onClick={() => void undo()}
/>
<ToolButton
icon={RotateCcwIcon}
label={t("textCommands.reset")}
iconOnly
disabled={!engine}
onClick={reset}
/>
</Toolbar>
<Status tone={message ? "ok" : "idle"}>
{message || t("textCommands.hint")}
</Status>
</DemoFrame>
);
}直接设置 text 演示了这个问题:粗体和紫色停留在旧的字母上。用命令插入 会让样式一直留在 “world” 上。
修改文字而不丢失样式
import { createTextCommands } from "fabricjs-document-engine";
const text = createTextCommands(engine);
text.insertText(title, 0, "New: ");
text.replaceText(title, 5, 9, "Big", { fontWeight: "bold" });
text.deleteText(title, 0, 5);
text.setTextStyle(title, 0, 3, { fill: "#6f00ff" });- 位置按字素计算,和光标一样,所以一个 emoji 算一个字母。
- 插入的文字沿用前一个字母的字体样式,或者使用你传入的
style。传入null则不带样式。 - 光标、选区和隐藏的 textarea 都会跟着修改移动,即使用户正在输入。
- 会触发
changed和text:changed,文档会被标记为未保存。 - 每个命令都是一步撤销。
setTextStyle 和 setSelectionStyles 一样把样式合并进一个范围,并替你记录撤销步骤。这些都适用于 IText、Textbox 及其子类中的 fabricjs 富文本。
多次修改合成一步
命令背后的函数 replaceTextRange 和 setTextRangeStyle 本身不记录任何步骤。把它们放进事务中:
import { replaceTextRange } from "fabricjs-document-engine";
engine.transaction("Rename", () => {
replaceTextRange(title, 0, 5, "Hello");
replaceTextRange(subtitle, 0, 0, "Draft: ");
});样式以 { start, end, style } 的区间移动,所以一次修改的开销取决于区间数量,而不是字母数量。shiftStyleRuns 也已导出,可以用在你自己的 fabricjs 富文本工具里。
手机上的输入见手机上的文字输入,撤销分组见撤销和重做。所有签名都在文字 API里,fabricjs 富文本的其他辅助函数也在那里。