Fabric.js Document Engine

搜索文档

查找页面或章节

EN

文本框宽度

查看 Markdown

保持你设定宽度的 Fabric.js 文本框,需要时还能限制高度。

Fabric.js Textbox 会在空格处换行,但比文本框还宽的单词永远不会被断开。这时 Fabric.js 文本自动换行会把整个文本框撑到这个单词的宽度(fabric.js #2376)。长链接、邮箱地址,或者没有空格的中文、日文句子,都会让文本框超出你的版面。BoundedTextbox 会保持你设定的宽度。

试一试

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

bounded-textbox-demo.tsx
"use client";
 
import { Textbox } from "fabric";
import {
  BoundedTextbox,
  type TextOverflow,
} from "fabricjs-document-engine/text";
import { ScissorsIcon, ShrinkIcon, TextIcon } 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 TEXT =
  "Read https://fabricjs-document-engine.jscrate.dev/docs/guides/textbox-width before you place long links in a design.";
 
const OVERFLOWS: TextOverflow[] = ["visible", "clip", "ellipsis"];
 
export default function BoundedTextboxDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const plainRef = useRef<Textbox | null>(null);
  const boundedRef = useRef<BoundedTextbox | null>(null);
  const [width, setWidth] = useState(180);
  const [overflow, setOverflow] = useState<TextOverflow>("ellipsis");
  const [shrink, setShrink] = useState(false);
  const [sizes, setSizes] = useState({ plain: 0, bounded: 0, font: 18 });
 
  useEffect(() => {
    if (!canvas) return;
    const common = {
      top: 40,
      fontSize: 18,
      originX: "left",
      originY: "top",
    } as const;
    const plain = new Textbox(TEXT, {
      ...common,
      left: 16,
      width: 180,
      fill: "#71717a",
    });
    const bounded = new BoundedTextbox(TEXT, {
      ...common,
      left: 256,
      width: 180,
      maxHeight: 110,
      overflow: "ellipsis",
      fill: "#0d0d0d",
    });
    plainRef.current = plain;
    boundedRef.current = bounded;
    canvas.add(plain, bounded);
    canvas.requestRenderAll();
    return () => {
      canvas.remove(plain, bounded);
    };
  }, [canvas]);
 
  useEffect(() => {
    const plain = plainRef.current;
    const bounded = boundedRef.current;
    if (!canvas || !plain || !bounded) return;
    plain.set({ width });
    bounded.set({
      width,
      overflow,
      fit: shrink ? "shrink" : "none",
      minFontSize: 8,
    });
    canvas.requestRenderAll();
    setSizes({
      plain: Math.round(plain.width),
      bounded: Math.round(bounded.width),
      font: Math.round(bounded.fontSize * 2) / 2,
    });
  }, [canvas, width, overflow, shrink]);
 
  return (
    <DemoFrame>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />
 
      <label className="flex items-center gap-3 text-[13px]">
        <span className="shrink-0">{t("textbox.width", { width })}</span>
        <input
          type="range"
          min={60}
          max={210}
          value={width}
          onChange={(event) => setWidth(Number(event.target.value))}
          className="w-full accent-[#6f00ff]"
        />
      </label>
 
      <Toolbar>
        {OVERFLOWS.map((value) => (
          <ToolButton
            key={value}
            icon={value === "clip" ? ScissorsIcon : TextIcon}
            label={t(`textbox.${value}`)}
            primary={overflow === value}
            onClick={() => setOverflow(value)}
          />
        ))}
        <ToolButton
          icon={ShrinkIcon}
          label={t("textbox.shrink")}
          primary={shrink}
          onClick={() => setShrink((value) => !value)}
        />
      </Toolbar>
 
      <Status tone="ok">
        {t("textbox.status", {
          plain: sizes.plain,
          bounded: sizes.bounded,
          font: sizes.font,
        })}
      </Status>
    </DemoFrame>
  );
}

灰色的是普通的 Fabric.js Textbox,黑色的是文字和宽度都相同的 BoundedTextbox。拖动滑块,比较它们的宽度。

保持宽度

import { createDocumentEngine } from "fabricjs-document-engine";
import { BoundedTextbox, textObjects } from "fabricjs-document-engine/text";
 
const engine = createDocumentEngine({ canvas, customObjects: textObjects });
 
canvas.add(
  new BoundedTextbox("A long link: https://example.com/a/very/long/path", {
    width: 240,
    fontSize: 20,
  })
);
  • 比文本框还宽的单词会单独占一行,并在字母之间断开,和 CSS overflow-wrap: anywhere 的 canvas文字换行方式相同。
  • 断行发生在字素之间,所以 emoji 和带重音的字母不会被拆开。
  • 样式、光标和选区在这些断行处照常工作。
  • 文本框不会比最宽的那个字母更窄。
  • breakWords: "never" 会关闭这种断行,让它和 Textbox 的 Fabric.js 文本自动换行一样。

用 customObjects: textObjects 注册这个类,含有 BoundedTextbox 的文档才能保存和加载。没有引擎时,调用一次 registerTextObjects()。

限制高度

设置 maxHeight,再选择超出部分的处理方式:

overflow看到的效果
visible显示所有行,和 Textbox 一样(默认)。
clip文字在 maxHeight 处被裁掉。
ellipsis超过 maxHeight 的行被隐藏,最后一行以省略号“…”结尾。
new BoundedTextbox(longText, {
  width: 240,
  maxHeight: 120,
  overflow: "ellipsis",
});

文字本身不会改变:复制、搜索和编辑仍然能看到每一个字。用户编辑时,文本框只裁剪、不画省略号,所以光标不会被盖住。SVG 和 PDF 导出使用同样的裁剪。

缩小字号直到放得下

fit: "shrink" 会降低字号,直到文字放进 maxHeight。它用二分查找按半磅尝试字号,所以 48px 的标题大约排版七次就够了,而不是四十次。

const title = new BoundedTextbox(headline, {
  width: 300,
  maxHeight: 90,
  fit: "shrink",
  minFontSize: 10,
  fontSize: 48,
});
 
console.log(title.fontSize, title.getBaseFontSize());

fontSize 读到的是实际绘制的字号,例如 31.5;getBaseFontSize() 读到的是你设定的字号 48。文档保存的是你设定的字号,而不是缩小后的字号。文字变短后,字号会长回这个大小。自带 fontSize 的字母按相同比例缩小。

字母相连的文字

阿拉伯文或带连字的字体,请加上 shaping: true,让断行和光标跟随相连的字母。见阿拉伯文和从右到左文字。

所有配置项都列在文字 API里。