Fabric.js Document Engine

搜索文档

查找页面或章节

EN

手机上的文字输入

查看 Markdown

用 Android 和 iOS 键盘在 Fabric.js 移动端编辑文字。

Fabric.js 通过一个隐藏的 textarea 接收输入。在手机上它工作得并不好(fabric.js #6588),所以 Fabric.js 移动端的文字编辑需要额外处理:

  • Android 键盘的每个按键都发送键码 229,Fabric 无法知道按了哪个键。
  • 键盘会修改光标之外的文字:自动更正会替换光标前的单词,联想会替换正在输入的单词。
  • 在空格键上滑动会移动光标,却没有任何按键事件。
  • textarea 的字号只有 1px,获得焦点时 iOS 会缩放页面。

Fabric 按它自己记录的光标推算每次修改,所以自动更正之后,IText 或 Textbox 里的字母或样式会落到错误的位置。

试一试

正在加载在线示例,代码在下方。

mobile-input-demo.tsx
"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"
autocorrecttrue
spellcheckfalse

Fabric 关闭了自动更正,因为它自己的处理无法正确应用更正。有了适配器,可以放心重新打开自动更正。用代码修改文字,请看用代码修改文字样式。所有类型列在文字 API里,Fabric.js 移动端的文字输入就完整了。