Fabric.js 通过一个隐藏的 textarea 接收输入。在手机上它工作得并不好(fabric.js #6588),所以 Fabric.js 移动端的文字编辑需要额外处理:
- Android 键盘的每个按键都发送键码 229,Fabric 无法知道按了哪个键。
- 键盘会修改光标之外的文字:自动更正会替换光标前的单词,联想会替换正在输入的单词。
- 在空格键上滑动会移动光标,却没有任何按键事件。
- textarea 的字号只有 1px,获得焦点时 iOS 会缩放页面。
Fabric 按它自己记录的光标推算每次修改,所以自动更正之后,IText 或 Textbox 里的字母或样式会落到错误的位置。
试一试
正在加载在线示例,代码在下方。
"use client";
import { type Canvas, IText } from "fabric";
import {
attachMobileTextInput,
type MobileTextInput,
} from "fabricjs-document-engine/text";
import { RotateCcwIcon, SmartphoneIcon, SpellCheckIcon } 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 BOLD = { fontWeight: "bold", fill: "#6f00ff" } as const;
function makeText() {
return new IText("im here", {
left: 40,
top: 110,
originX: "left",
originY: "top",
fontSize: 44,
fill: "#0d0d0d",
styles: { 0: { 3: BOLD, 4: BOLD, 5: BOLD, 6: BOLD } },
});
}
function boldLetters(text: IText) {
return [...text.text]
.filter((_, index) => text.styles[0]?.[index]?.fontWeight === "bold")
.join("");
}
export default function MobileInputDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const adapterRef = useRef<MobileTextInput | null>(null);
const textRef = useRef<IText | null>(null);
const [adapter, setAdapter] = useState(true);
const [result, setResult] = useState<{ text: string; bold: string } | null>(
null
);
function reset(target: Canvas) {
if (textRef.current) target.remove(textRef.current);
const text = makeText();
textRef.current = text;
target.add(text);
target.requestRenderAll();
setResult(null);
}
useEffect(() => {
if (!canvas) return;
if (adapter) adapterRef.current = attachMobileTextInput(canvas);
reset(canvas);
return () => {
adapterRef.current?.detach();
adapterRef.current = null;
};
}, [canvas, adapter]);
function autocorrect() {
const text = textRef.current;
if (!canvas || !text) return;
canvas.setActiveObject(text);
text.enterEditing();
text.setSelectionStart(text.text.length);
text.setSelectionEnd(text.text.length);
const textarea = text.hiddenTextarea;
if (!textarea) return;
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
keyCode: 229,
key: "Unidentified",
} as KeyboardEventInit)
);
textarea.value = "I'm here";
textarea.setSelectionRange(8, 8);
textarea.dispatchEvent(
new InputEvent("input", {
inputType: "insertReplacementText",
bubbles: true,
})
);
text.exitEditing();
canvas.requestRenderAll();
setResult({ text: text.text, bold: boldLetters(text) });
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={SmartphoneIcon}
label={adapter ? t("mobile.on") : t("mobile.off")}
primary={adapter}
onClick={() => setAdapter((value) => !value)}
/>
<ToolButton
icon={SpellCheckIcon}
label={t("mobile.autocorrect")}
disabled={!canvas}
onClick={autocorrect}
/>
<ToolButton
icon={RotateCcwIcon}
label={t("mobile.reset")}
disabled={!canvas}
onClick={() => canvas && reset(canvas)}
/>
</Toolbar>
<Status tone={!result ? "idle" : result.bold === "here" ? "ok" : "error"}>
{result
? t("mobile.result", { text: result.text, bold: result.bold || "—" })
: t("mobile.hint")}
</Status>
</DemoFrame>
);
}按钮会模拟 Android 键盘把 “im” 改成 “I'm” 的过程:修改光标之前的文字。关闭适配器时,粗体样式会移到错误的字母上。
开启适配器
import { attachMobileTextInput } from "fabricjs-document-engine/text";
const mobileInput = attachMobileTextInput(canvas);
mobileInput.detach();在用户开始编辑之前调用一次。它对画布上所有的 IText、Textbox 及其子类都有效,包括之后添加的对象。
- 每次修改都从文字本身读取:新旧文字相同的开头和结尾之间,就是真正改变的部分。样式跟着字母移动。
- 光标跟随 textarea,包括 Android 键盘自己做出的移动。
- textarea 在点击时获得焦点,所以 iOS 和 Android 都会弹出键盘。
- textarea 使用 16px 字号,iOS 不会缩放页面。
- 键盘弹出、页面滚动时,textarea 始终在光标处,输入法会在正确的位置显示候选词。
detach() 会恢复 Fabric 自己的处理方式。
键盘配置
attachMobileTextInput(canvas, {
inputMode: "text",
enterKeyHint: "done",
autocapitalize: "sentences",
autocorrect: true,
});| 配置项 | 默认值 |
|---|---|
inputMode | "text" |
enterKeyHint | "enter" |
autocapitalize | "sentences" |
autocorrect | true |
spellcheck | false |
Fabric 关闭了自动更正,因为它自己的处理无法正确应用更正。有了适配器,可以放心重新打开自动更正。用代码修改文字,请看用代码修改文字样式。所有类型列在文字 API里,Fabric.js 移动端的文字输入就完整了。