Overview
The WeaveTextV2Node class represents a text element on the Weave.js canvas, with editing drawn natively on the canvas instead of a DOM textarea overlay. It is a drop-in replacement for WeaveTextNode; the existing class is unchanged and stays available.
Built on a Konva.Shape with its own layout engine, it keeps the editing and idle appearance
identical, and supports opt-in rich text.
The class extends the WeaveNode class
Type
This node nodeType property value is text, the same as WeaveTextNode.
|
Existing documents load and render without migration. Register |
Instantiation
const node = new WeaveTextV2Node({
config: {
edition: { outline: { color: "#0074ff", width: 1, padding: 0 } },
sizeWarning: { threshold: 200000 },
},
});
Differences from WeaveTextNode
-
Renders through
WeaveTextV2Shape(aKonva.Shape) with Konva.Text-compatible getters (text(),fontSize(),getTextWidth(), …). TheonEnterTextNodeEditModeandonExitTextNodeEditModeevent payloadnodeis aWeaveTextV2Shape, not aKonva.Text. -
Editing happens on the canvas: a hidden proxy textarea handles typing, IME and clipboard, and the caret and selection are painted on the utility layer, together with an outline of the text box (useful with fixed width or height). There is no DOM overlay.
-
Entering edit mode on existing text selects all of it (like Figma), so typing replaces it; a newly created node starts with the caret.
-
Selection supports click, drag, double-click (word), triple-click (paragraph), arrows, Home/End and Up/Down on visual lines.
-
Undo/redo is local to the edit session (typing, deletion, paste and IME composition each coalesce into one step). Leaving edit mode commits a single
updateNode, skipped when the text is unchanged. While editing, keydown does not propagate to the page, so app-level undo does not fire;window.weaveTextEditingholds the editing node id. -
The pointer is a text cursor (I-beam) over the text box while editing and the default cursor outside it, also while dragging a selection. Elements with
data-weave-text-edit-keepand touch pointers keep their own cursor. It is restored when the session ends. -
Clipboard is plain text only. Large pastes are laid out in time slices.
-
While editing, the stage pans to keep the caret in view; the margin is
edition.followPadding. -
A pointer press inside an element with the attribute
data-weave-text-edit-keepdoes not end the edit session, so an app toolbar or sidebar can apply formatting commands to the live selection. Mouse-down on such an element keeps focus on the editor, except on form fields (input,textarea,select,contenteditable), which take focus normally. Portaled popups (for example a font picker) need the attribute too. -
All four layouts (
smart,auto-all,auto-height,fixed) keep their behavior. Auto-sized layouts measure from the engine and no longer depend on the zoom level. -
WeaveTextToolAction works with it unchanged.
Content model
Content is stored as blocks (paragraphs) of runs ({ t, m }: text and marks). The text prop
stays as a derived mirror. A node without blocks is synthesized from text on load without
rewriting the document; a whole-text link becomes a full-range link mark.
Marks are bold, italic, underline, strike, color, size and family lineHeight (a multiplier of the run font size, clamped to 0.5 to 4) and link (a per-run URL). size is a ratio of the node fontSize, so resizing the node
or calling setBaseFontSize scales every run. Blocks may carry kind (ul or ol), depth
and start to form lists, and align (left, center or right) for the paragraph. Documents without marks and lists are byte-identical to before.
Marks and lists already present in a document always render and are preserved when the text is
edited, whatever richText.enabled is, so every client shows the same document.
Rich text
Rich text is switched on per node with the boolean attribute richText. Nodes without the
attribute follow config.richText.enabled (default false), so you can offer a plain and a rich
text tool in the same app (see WeaveTextToolAction).
Turning it off for a node only removes the commands: marks and lists already in it still render. The toolbar is yours: the node only
exposes commands, a format query and events.
const handler = instance.getNodeHandler<WeaveTextV2Node>("text");
handler.toggleMark(node, "bold"); // whole text, or the editor selection
handler.applyMark(node, "color", "#ff0000", { start: 0, end: 5 });
handler.applyMark(node, "size", 24); // px
handler.toggleList(node, "ul");
handler.indent(node);
handler.setBaseFontSize(node, 32); // scales every run
const format = handler.getSelectionFormat(node);
-
While the node is being edited the commands act on the editor selection (the
rangeargument is ignored) and each one is a single session-undo entry. At a collapsed caret, mark commands set the marks of the next typed text. -
On a selected node that is not being edited they act on
range(UTF-16 offsets intotext, default: the whole text) and are oneupdateNode, so one Weave undo step. This lets an application edit text from a modal. -
When rich text is off for the node every command returns
falseand does nothing.isRich(node)reports it, so a toolbar can show or hide itself.isRichTextEnabled()without arguments returns the handler default (config.richText.enabled).
| Method | Description |
|---|---|
|
Set a mark ( |
|
Toggle a boolean mark ( |
|
Align every paragraph the range touches ( |
|
Toggle |
|
Change list depth. Outdent at depth 0 removes the list marker. |
|
Link a range to an absolute http(s) URL (others return |
|
Set the node |
|
Per mark (including |
'none' |
'some', value? }`, |
When editing with rich text enabled: Ctrl/Cmd + B, I, U toggle marks;
Enter continues a list item (and the formatting of the text before it) and exits on an empty one; Backspace at the start of an item
outdents it or removes the marker; Tab / Shift+Tab change the depth. Pasting inserts
plain text with the marks at the caret. With it disabled, Enter in a list item still creates
an item with the same kind and depth.
Link runs are drawn with link.defaultColor and an underline; the hovered run uses link.hoverColor
and the cursor becomes a pointer only over the run. Ctrl/Cmd + click opens it, as does a
click on an already-selected node. Only http(s) URLs render and open. A node-level link (see
Hyperlinks) takes precedence. Links render
whatever richText.enabled is; creating and removing them is a command.
A line is as tall as its tallest run: the lineHeight mark of a run, or the node lineHeight,
times the run font size. Only the lines that contain the marked text change height, and a smaller
value next to a taller run on the same line has no visible effect. Empty paragraphs use the node
lineHeight. The node align and lineHeight are defaults: changing them does not clear paragraph
or run values. Vertical alignment (verticalAlign) stays a setting of the whole node.
Lists show markers in a gutter (disc, circle, square; 1., a., i. by depth). Numbering is
continuous across consecutive same-depth items and restarts after a plain paragraph or a shallower
item; start sets the first number. Markers are not part of text.
Images export with the formatting. Exporting to PDF rasterizes each frame, so it follows the image. Register every font family you use in runs on the server as well.
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
|
|
Config parameters for the node. |
Configuration
Same properties as WeaveTextNode, except edition, plus:
|
|
| Property | Default | Description |
|---|---|---|
|
|
Draw an outline of the text box while editing. |
|
|
Outline colour. |
|
|
Outline width in screen pixels (constant across zoom levels). |
|
|
Gap between the text box and the outline, in screen pixels. |
|
|
While editing, the stage pans to keep the caret inside the container. This is the margin kept between the caret and each edge, in screen pixels. Raise a side when app UI (toolbars, sidebars) covers that edge. |
|
|
Default for nodes without a |
|
|
List indent per depth, as a multiple of |
|
|
Serialized size of |
Events
| Event | Payload | Description |
|---|---|---|
|
|
An edit session started. |
|
|
An edit session ended and its content was committed. |
|
|
The selection, caret or content changed while editing (throttled). |
|
`+{ node, format, reason: 'command' |
'undo' |
'redo' |
'base-size' }+` |
A format command, undo, redo or base size change ran. |
|
|
The serialized |
Hyperlinks
Same behavior and methods as WeaveTextNode:
getLink, setLink and removeLink. Removing the link also removes the link
underline. Link hover colour and the pointer cursor only show while Ctrl/Cmd is held, and
Ctrl/Cmd + click opens the URL. While it is held the blue hover halo of the node is hidden.
Exports
WeaveTextV2Node, WeaveTextV2Shape, TextLayoutEngine, TextEditSession,
TextEditModel and the related types. TextEditSession works on a TextEditTarget
and a TextEditHost, so other shapes can reuse the layout engine and the editor. The pure model
functions (applyMark, toggleList, listMarkers, getSelectionFormat, …) and
clearSyntheticItalicCache are exported too.
Server side
Measurement and painting work on the node-canvas backend (setupCanvasBackend()) and were also
verified on the skia-canvas backend (setupSkiaBackend()). Register fonts in your app; they are not
registered by the SDK.
Italic on the server
Browsers slant (synthesize) italic text when a font family has no italic file. skia-canvas and
node-canvas do not: the text comes out upright. WeaveTextV2Shape detects this on the server by
rendering a small probe with the run’s font, and when the italic does not render slanted it applies
the same slant browsers use (a shear of about 14 degrees, on the glyphs only). Families that have a
real italic file are drawn with it, unchanged. It works the same on both backends and never runs in a
browser. Layout is not affected: wrapping and line breaks are identical to the canvas.
The result is cached per font. If you register fonts after the first render, call
clearSyntheticItalicCache() so it is probed again.
| The slant is close to, but not pixel-identical with, the one Chrome draws. Server images of text with italic runs in families without an italic file now come out slanted instead of upright. |