API reference
Every class, function, option and type the library exports, generated from its types and grouped by what you use them for. Version 1.0.1.
You won't need most of this. A typical app uses a few names: configureRichText, enableRichText (or createCanvasRichText on a plain canvas), RichText boxes, the formatting commands, and the formatting report for its toolbar.
Start with Getting started or the demo, where every feature shows its code. Come back here to look up an exact name or option.
@mycreativeshop/rich-text
Rich text on a plain HTML canvas, no Konva: createCanvasRichText and CanvasTextBox, settings, fonts and documents.
@mycreativeshop/rich-text/konva
Rich text for Konva: the RichText node, enableRichText, export, and the same document and settings types as the plain canvas.
Setup
Settings for every text box, and rich text on a Konva stage or a plain canvas.
configureRichText
function Konva or canvasconfigureRichText(settings: RichTextSettings): void
Set where the library fetches fonts, fallback fonts, the dictionary and emoji from.
| Parameter | Type | Description |
|---|---|---|
settings |
RichTextSettings |
RichTextSettings
interface Konva or canvasinterface RichTextSettings
The library's settings, given to configureRichText: where fonts come from (and a lookup for your own), what to do
when one can't load, fallback fonts, spellcheck dictionaries, emoji, the render mode and diagnostics. Each call
changes only what it names.
emoji?: null | EmojiSource
Emoji images (vector mode). null = the browser draws emoji. Default: our Noto set (DEFAULT_EMOJI_BASE_URL).
fallbackFonts?: false | { cssUrl: (request: FallbackFontRequest) => string }
Fonts for characters a design font lacks (vector mode). false turns them off: those characters
are left to the browser. Default: Google's Noto fonts, only the missing characters.
fonts?: { cssUrl?: (face: FontFaceRequest) => null | string; lookup?: null | FontLookup; onFontError?: null | (error: FontError) => void }
Where fonts come from: the address for a family's faces, a lookup for your own fonts, and a callback when one can't load.
licenseKey?: null | string
Your license key, from your licenses page at mycreativeshop.com. Without one, or on a domain the license does not cover, everything still works and each text box shows a small mark: "Development" on localhost, staging and other domains not on your license, "Unlicensed" on any other site. Can be set at any time; null removes it.
renderMode?: TextRenderingMode
How glyphs are drawn: "native" (the browser's text drawing) or "vector" (HarfBuzz outlines — identical on screen, in print and on a server). Default "vector". Fixed once the first text box exists: set it before creating any (a later change is ignored, with a console warning).
spellcheck?: { aff: string; dic: string }
Hunspell dictionary files for spellcheck, fetched from the page. Default: the US English dictionary bundled with the library. Loaded once, when spellcheck starts.
enableRichText
function Plain canvasenableRichText(stage: Stage, options?: EnableRichTextOptions): RichTextHandle
Turns on rich text editing for a Konva stage in one call.
Sets up the editing manager (caret, keyboard, clipboard, spellcheck, undo) and the handler for
text inside Konva groups. Defaults: the text lives on the stage's first layer, and any
Konva.Transformer on the stage is treated as the selection transformer.
Text boxes attach on their own: add a RichText anywhere on the stage (a layer, a group,
a group added later) and it becomes editable; remove or destroy it and it is released. Selection
follows your own transformer: when you set its nodes (tr.nodes([...])), the library sees the
change, so there is nothing else to call.
Pass { interactive: false } for a render-only stage (an offscreen copy, a preview, print): text
lays out and draws, and API calls work, but nothing listens to the pointer or keyboard.
| Parameter | Type | Description |
|---|---|---|
stage |
Stage |
The Konva stage. It must already have at least one layer, unless you pass |
options |
EnableRichTextOptions (optional) |
Optional settings; see EnableRichTextOptions. |
Returns RichTextHandle
A RichTextHandle: manager for formatting, undo/redo and clipboard commands, and
destroy() to remove everything this call added.
import Konva from "konva";
import { enableRichText, RichText, createDocumentFromText } from "@mycreativeshop/rich-text/konva";
const stage = new Konva.Stage({ container: "editor", width: 800, height: 600 });
const layer = new Konva.Layer();
stage.add(layer);
const tr = new Konva.Transformer();
layer.add(tr);
const rt = enableRichText(stage);
const box = new RichText({ x: 50, y: 50, width: 300, document: createDocumentFromText("Hello") });
layer.add(box); // editable now
// Your own selection: put the clicked box on the transformer
stage.on("click tap", (e) => {
tr.nodes(e.target === stage ? [] : [e.target]);
});
// A toolbar button: bold the selected box, or the selected words while typing
boldButton.onclick = () => rt.manager.toggleBold();
EnableRichTextOptions
interface Plain canvasinterface EnableRichTextOptions extends Omit<RichTextManagerConfig, "stage" | "layer" | "transformerSelector" | "groupedTransformerSelector">
Options for enableRichText. Every option is optional.
Spellcheck, the context menu, the editing callbacks and the rest, plus layer and transformers below.
context?: RichTextContext<RichText>
The editor session this manager belongs to (createRichTextContext()): managers in one context
share undo and editing state (an editor with several pages, one manager per page); managers in
different contexts never see each other (an offscreen copy, a preview). Default: the page-wide
default context.
disableInternalUndoRedoShortcuts?: boolean
"library" history mode only: true = the library does not act on Ctrl+Z / Ctrl+Y (Cmd on Mac)
while a box is being edited; the app binds those keys and calls manager.undo() / redo().
Not needed in "host" mode: there the library keeps no list, so its keys change nothing and the
app's own undo (which restores steps with context.restore) is the only one.
enableContextMenu?: boolean
Show the library's right-click menu on the box being edited: spelling suggestions, Add to dictionary and Ignore (on a misspelled word), then Cut, Copy and Paste. Set to false to handle right-clicks yourself; right-clicks anywhere else are always left to your app.
enableSpellcheck?: boolean
Underline misspelled words while editing, with suggestions in the right-click menu. The English dictionary starts downloading in the background as soon as the manager is created.
interactive?: boolean
false = render-only: boxes lay out, draw and take API calls (setText, formatting…), but nothing listens to the pointer or keyboard — no stage, window or box handlers, no edit mode. For offscreen copies, previews and print. Default true.
layer?: Layer
The layer the text lives on. Default: the stage's first layer.
onBeforeMouseDown?: (event: KonvaEventObject<MouseEvent | TouchEvent>, richText: RichText) => boolean
Callback to intercept mousedown events on RichText nodes BEFORE the library processes them.
Return true to prevent the library from handling the event (e.g., to prevent exit mode).
Return false to allow normal library processing.
This is useful when you want to prevent edit mode from exiting under specific conditions, such as when clicking on custom controls or specific areas within a RichText node.
const manager = new RichTextManager({
// ... other config
onBeforeMouseDown: (e, richText) => {
const pos = richText.getRelativePointerPosition();
// Prevent exit if clicking in top-left control area
if (pos.x < 50 && pos.y < 50) {
console.log('Clicked control area - staying in edit mode');
return true; // Prevent library from processing
}
return false; // Allow normal processing
}
});
onClipboardAccessDenied?: () => void
Callback fired when clipboard access is denied during a paste operation. This typically happens when the user blocks clipboard permission in the browser, or when pasting via context menu without granted permission.
Use this to show a user-friendly message suggesting Ctrl+V or updating permissions.
Note: This callback is NOT fired if there is content in the internal clipboard (content copied from within the editor), since that content will be pasted instead.
const manager = new RichTextManager({
// ... other config
onClipboardAccessDenied: () => {
showToast("Clipboard access denied. Use Ctrl+V to paste external content.");
}
});
onDocumentChange?: (node: RichText) => void
A box's text or formatting changed - for autosave or an "unsaved changes" mark. Called after every change
people make (typing, formatting, a resize, Story Mode) and every undo and redo, once per change: each keystroke
too, so debounce a save. Changes your own code makes outside the library's commands are not reported.
Every manager hears its own boxes; context.onDocumentChange hears every box of a context.
onEmptyBlur?: (node: RichText) => void
Callback fired when a user exits a text box that is empty (contains only whitespace/line breaks). The consuming application can decide how to handle this (e.g., remove the node, detach from transformer).
The node's getLastNonEmptyDocument() method can be used to retrieve the last meaningful content for undo.
const manager = new RichTextManager({
// ... other config
onEmptyBlur: (node) => {
// Get last non-empty content for undo
const lastDoc = node.getLastNonEmptyDocument();
if (lastDoc) {
myUndoStack.push({
type: 'delete',
document: lastDoc,
x: node.x(),
y: node.y(),
});
}
// Detach from manager (cleans up internal state)
manager.detachRichText(node);
// Remove from transformer and layer
const transformer = stage.findOne('#myTransformer') as Konva.Transformer;
const currentNodes = transformer.nodes().filter(n => n !== node);
transformer.nodes(currentNodes);
node.destroy();
layer.draw();
}
});
onEnterEditMode?: (node: RichText) => void
Callback fired when a RichText node enters edit mode. Useful for showing formatting toolbars, tracking analytics, or updating UI state.
const manager = new RichTextManager({
// ... other config
onEnterEditMode: (node) => {
showFormattingToolbar();
trackActivity('richtext_edit_started');
}
});
onExitEditMode?: (node: RichText) => void
Callback fired when a RichText node exits edit mode. Useful for hiding formatting toolbars, saving changes, or updating UI state.
const manager = new RichTextManager({
// ... other config
onExitEditMode: (node) => {
hideFormattingToolbar();
saveChanges(node);
}
});
onSelectionChange?: (selectedRichTexts: RichText[]) => void
Called whenever the selected text boxes change, with the boxes now selected (an empty array when none is). Other selected shapes are not included.
onStoryModeEnter?: (node: RichText) => void
Callback fired when Story Mode is entered. Story Mode provides a fullscreen text editing overlay, particularly useful on mobile devices. Use this to hide UI elements, pause other interactions, etc.
const manager = new RichTextManager({
// ... other config
onStoryModeEnter: (node) => {
hideFormattingToolbar();
disableCanvasInteractions();
}
});
onStoryModeExit?: (node: RichText, wasSaved: boolean) => void
Callback fired when Story Mode is exited (either saved or cancelled). Use this to restore UI state, show toolbars again, etc.
const manager = new RichTextManager({
// ... other config
onStoryModeExit: (node, wasSaved) => {
enableCanvasInteractions();
if (wasSaved) {
trackActivity('story_mode_edit_saved');
}
}
});
storyModePlacement?: (box: RichText) => undefined | StoryModePlacement
Where a box is on screen for Story Mode's overlay (default: worked out from the stage). See StoryModeConfig.placement.
storyModeScale?: (box: RichText) => number
How large a box's text looks for Story Mode's attached panel (default: from the stage). See StoryModeConfig.scale.
transformers?: { groupChild?: string; selection?: string }
Where the app's transformers are (selectors). selection: the one the app attaches to selected
nodes — default "Transformer", i.e. any Konva.Transformer on the stage. groupChild: the one for
a text box selected INSIDE a group — default: the selection transformer.
upgradeOlderText?: TextUpgradePolicy
When text saved by an older version of the library moves to the latest rendering (it may shift slightly): "never" (default) — it keeps rendering exactly as it was saved, edited or not; "immediately" — as it loads. New text is always the latest. See the Upgrading guide.
RichTextHandle
interface Plain canvasinterface RichTextHandle
What enableRichText returns: the manager for the stage, and a destroy() that undoes the setup.
readonly manager: RichTextManager
Editing, formatting and text operations: toggleBold(), setText(), undo(), copy() and the rest.
destroy(): void
Remove everything this added — listeners, the caret, its boxes' registrations. The stage stays.
createCanvasRichText
function KonvacreateCanvasRichText(options?: CanvasRichTextOptions): CanvasRichText
Adds editable rich text to a plain HTML canvas, without Konva.
Your app keeps its own render loop and its own objects. The library supplies text boxes that
paint themselves (paint), an editor that takes pointer input in your scene's coordinates and
keys and paste from the window, and an overlay with the caret, the text selection and the
selected box's resize handles (paintOverlay).
Forward the canvas's pointer events (fromEvent converts a DOM event), repaint when onChange
asks, and await ready() before exporting an image so every font is drawn.
| Parameter | Type | Description |
|---|---|---|
options |
CanvasRichTextOptions (optional) |
Optional settings; see CanvasRichTextOptions. |
Returns CanvasRichText
The CanvasRichText for this canvas. Call destroy() when you are done with it.
// createCanvasRichText and createDocumentFromText: imported from @mycreativeshop/rich-text
const canvas = document.querySelector("canvas")!;
const ctx = canvas.getContext("2d")!;
const ratio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * ratio;
canvas.height = canvas.clientHeight * ratio;
let frame = 0;
const rt = createCanvasRichText({
onChange: () => { frame ||= requestAnimationFrame(render); },
});
function render() {
frame = 0;
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
rt.paint(ctx); // the text boxes
rt.paintOverlay(ctx); // caret, selection and handles, on top
}
rt.addBox({ x: 40, y: 40, width: 400, document: createDocumentFromText("Grand Opening") });
// Scene coordinates are CSS pixels here, so map canvas pixels back by the ratio
const scene = new DOMMatrix().scale(ratio);
const at = (e: MouseEvent) => rt.fromEvent(e, canvas, scene);
canvas.addEventListener("pointerdown", (e) => rt.pointerDown(at(e)));
// Move and release on the window, so a drag that leaves the canvas still ends
window.addEventListener("pointermove", (e) => {
rt.pointerMove(at(e));
canvas.style.cursor = rt.cursor();
});
window.addEventListener("pointerup", (e) => rt.pointerUp(at(e)));
canvas.addEventListener("click", (e) => rt.click(at(e)));
canvas.addEventListener("contextmenu", (e) => {
if (rt.contextMenu(at(e), { x: e.clientX, y: e.clientY }).handled) e.preventDefault();
});
CanvasRichTextOptions
interface Konvainterface CanvasRichTextOptions
Options for createCanvasRichText. Every option is optional.
caretWidth?: number
The caret's width in CSS pixels (default 1.5), as Konva's.
enableContextMenu?: boolean
The right-click menu while editing (default on): spelling suggestions, then Cut / Copy / Paste. See contextMenu.
enableSpellcheck?: boolean
Spellcheck while a box is edited (default on): misspelled words get a red squiggle, and the right-click menu
(contextMenu) offers suggestions, Add to dictionary and Ignore. The dictionary (US English) downloads the
first time it is needed. False: no spellcheck.
history?: HistoryMode
Who keeps the undo steps: "library" (default) - undo, redo and their keys work by themselves; "host" - every step
goes to onHistoryStep, for your app's own undo stack beside its other changes, and restore applies one;
"none" - no history (previews, print).
objectControls?: boolean | { fill?: string; size?: number; stroke?: string }
The object controls (default on): a click selects the box it lands on (a second click edits it), a drag
moves it, and handles resize it - corners scale the type uniformly (Alt: about the center), sides change
the width, never narrower than the longest word. The look is yours (handle size and colors); the behavior
is the same on every surface. False: a host with its own object UI selects (select), moves and
resizes itself (the resize rules are in @mycreativeshop/rich-text/kit).
onChange?: () => void
Called whenever the text boxes need repainting: schedule your render (e.g. requestAnimationFrame).
onDocumentChange?: (box: CanvasTextBox) => void
A box's text or formatting changed - for autosave or an "unsaved changes" mark. Called after every change people
make (typing, formatting, a resize, Story Mode) and every undo, redo and restore, once per change: each
keystroke too, so debounce a save. Changes your own code makes outside the library's commands are not reported.
onEmptyBlur?: (box: CanvasTextBox) => void
A box was left EMPTY when editing ended (before onExitEditMode) - e.g. remove it.
onEnterEditMode?: (box: CanvasTextBox) => void
A box entered edit mode.
onExitEditMode?: (box: CanvasTextBox) => void
A box left edit mode.
onHistoryStep?: (step: HistoryStep) => void
"host" history: every step, as it happens - keep it in your app's undo stack.
onStoryModeEnter?: (box: CanvasTextBox) => void
Story Mode opened on a box (enterStoryMode).
onStoryModeExit?: (box: CanvasTextBox, wasSaved: boolean) => void
Story Mode closed: wasSaved true for exitStoryMode or its Done button, false for cancel.
pixelRatio?: number
Device pixels per CSS pixel your canvas is drawn at - the scale you apply for a sharp canvas (canvas.width = css width × ratio; ctx.scale(ratio, ratio)). Default the screen's (window.devicePixelRatio). The caret and the handles are sized in CSS pixels on screen, as on Konva.
selectionColor?: string
The selection highlight (default the Konva editor's, translucent blue).
storyModePlacement?: (box: CanvasTextBox) => undefined | StoryModePlacement
Where a box is on screen, so Story Mode's overlay opens over it on desktop: your canvas element and the box's rectangle in its CSS pixels. Without it the overlay opens centered. See StoryModeConfig.placement.
storyModeScale?: (box: CanvasTextBox) => number
How large a box's text looks on screen (your zoom), for Story Mode's attached panel. Default: 1.
upgradeOlderText?: TextUpgradePolicy
When text saved by an older version of the library moves to the latest rendering (it may shift slightly): "never" (default) - it keeps rendering exactly as it was saved, edited or not; "immediately" - as it loads. New text is always the latest. See the Upgrading guide.
CanvasPointer
interface Konvainterface CanvasPointer
A pointer event, in the host's scene coordinates (the space box x / y are in).
altKey?: boolean
Alt: a resize grows about the box's center.
pointerType?: string
"mouse" | "pen" | "touch" (touch gets larger thresholds).
shiftKey?: boolean
Shift held: a click extends the text selection.
timeStamp?: number
The event's own timestamp (performance.now() clock).
x: number
Horizontal position, in scene coordinates.
y: number
Vertical position, in scene coordinates.
StoryModePlacement
interface Konva or canvasinterface StoryModePlacement
Where a box is on screen, for the overlay to open over it: the element the canvas is drawn in, and the box's rectangle inside that element, in its CSS pixels.
box: { height: number; width: number; x: number; y: number }
container: HTMLElement
Text boxes
The text box on each surface, and what you create one with.
RichText
class Plain canvasclass RichText extends Shape, BoxApi
A rich text box on a Konva stage: a Konva.Shape that holds a rich text document (paragraphs of
text with mixed fonts, sizes, colors, lists and effects) and lays it out and draws it on the canvas.
You set the box's width; its height follows the text. Add it to any layer or group like another
Konva node, and save it with Konva's own toJSON(): the document is stored in the node's attributes.
Once enableRichText is on for the stage, every RichText on it can be selected with your
transformer and edited in place (a click on a box that is already selected starts typing).
Without it, a RichText still lays out and
draws, and its methods (such as setDocument) still work, which suits previews and print.
Wait for richTextReady before exporting an image or printing, so every font is drawn.
const box = new RichText({
x: 40,
y: 40,
width: 400,
document: createDocumentFromText("Grand Opening"),
});
layer.add(box);
new (config: RichTextConfig): RichText
Creates a text box. It starts loading the fonts its document uses right away (without
blocking) and lays the text out once they are ready. Await fontsReady() on the box, or
richTextReady for a whole stage, when you need the final layout.
| Parameter | Type | Description |
|---|---|---|
config |
RichTextConfig |
Konva shape settings (x, y, width, rotation, draggable, your own attributes) plus the text box's own; see RichTextConfig. |
Returns RichText
bakeScale(scaleX: number, scaleY: number): void
PUBLIC API: Manually bake scale into document attributes Useful for programmatic transforms or custom transformer setups
| Parameter | Type | Description |
|---|---|---|
scaleX |
number |
|
scaleY |
number |
compareWithNativeLayout(): Promise<null | NativeLayoutComparison>
In vector text rendering mode, checks whether this box would break its text into different lines in native mode (the browser's own text drawing).
The two modes differ only for characters the box's fonts lack, and for emoji: vector mode draws them from the library's own fallback fonts and emoji set, native mode from the device's fonts. A box without any such character cannot differ, and the method returns null at once. Otherwise it lays out a hidden copy the native way and compares the lines; this box does not change.
Returns Promise<null | NativeLayoutComparison>
null when not in vector mode or when the modes cannot differ; otherwise the lines in each mode, whether they differ, and the characters drawn from the library's fallback fonts (which can look different between the modes even when the lines match).
copyfitDocument(newDocument: RichTextDocument, options?: { containerHeight?: number; containerWidth?: number; maxIterations?: number; minScale?: number; tolerance?: number }): number
Copyfit a new document into this node by scaling typography to fit within fixed dimensions. The node's x, y, width, and height remain unchanged. The document is scaled to fit. Uses binary search to find the optimal scale factor.
| Parameter | Type | Description |
|---|---|---|
newDocument |
RichTextDocument |
The document to fit into this node |
options |
{ containerHeight?: number; containerWidth?: number; maxIterations?: number; minScale?: number; tolerance?: number } (optional) |
Optional configuration for the copyfitting algorithm |
Returns number
The final scale factor that was applied (1.0 = no scaling, 0.5 = 50% scale, etc.)
const richText = new RichText({ x: 100, y: 100, width: 300, height: 200, document: oldDoc });
const scaleFactor = richText.copyfitDocument(newLargeDoc);
console.log(`Content scaled to ${scaleFactor * 100}% to fit`);
destroy(): this
Override destroy to clean up font loading state
Returns this
fontsReady(): Promise<void>
Resolves when this node's most recent async initialization (font loading
- initial layout) has finished. Useful for consumers that must rasterize the canvas only after fonts are loaded — e.g. headless PDF/image export. load() starts initialization in the background; await this to know when it's safe to capture the stage.
Never rejects — _initializeAsync handles its own errors internally (transitions to ERROR state) so awaiters get a clean settle either way.
Returns Promise<void>
getCurveAmount(): number
How far the text bends, in degrees of arc: positive bends up, negative bends down, 180 is a half circle.
Default 180. For text set on a circle of a fixed radius (DocumentCurve.arcRadius), the bend worked out
from the text's width.
Returns number
getCurveEnabled(): boolean
Whether the text is drawn on a curve.
Returns boolean
getDocument(): RichTextDocument
The box's document. This is the live object the box draws, not a copy: don't change it in place. To change
the text, edit a copy and pass it to setDocument.
Returns RichTextDocument
The box's current document.
getLastNonEmptyDocument(): null | RichTextDocument
Get the last non-empty document state Useful for undo operations when a text box is deleted
Returns null | RichTextDocument
The last document that contained meaningful content, or null if never had content
getLayout(): TextLayout
The box's current layout — its lines, every letter's position and metrics, its content width and text height, its longest word — as last laid out. Read-only: for hosts that measure what is on the canvas (where the ink is, which colors are drawn). Never lays out.
Returns TextLayout
getLongestWordWidth(): number
The width of the widest single word in the text, in pixels, letter spacing and list indent included: the narrowest the box can be without a word overflowing it. Useful as a minimum width when resizing. Measured with the fonts loaded so far.
Returns number
getMinimumFontSize(): null | number
The smallest font size the text is drawn at, in pixels: each run's own size, or its paragraph's where the run sets none. Useful to keep text legible, for example to stop shrinking at a minimum size.
Returns null | number
The smallest size, or null when the document has no paragraphs.
getTransformer(): null | Transformer
Get the current transformer instance
Returns null | Transformer
The current transformer or null if none is set
getUsedColors(): UsedColors
Get all unique colors used in this RichText node's document Returns both text colors and stroke colors as separate arrays All colors are deduplicated and normalized to lowercase
Returns UsedColors
Object containing arrays of unique text colors and stroke colors
getUsedFonts(): string[]
Get all unique font families used in this RichText node's document
Returns string[]
Array of font family names used in the document
layoutAt(doc: RichTextDocument, width: number): TextLayout & { box: { height: number; width: number } }
Lay doc out at width the way this box would — its fonts, its measurer, its fallback size —
and return the result WITHOUT applying it: the box's document, size and lines are untouched.
For trial layouts (copyfit's scales, an app's "does this fit?"). Measures with the fonts already
loaded, as the box itself does.
| Parameter | Type | Description |
|---|---|---|
doc |
RichTextDocument |
|
width |
number |
Returns TextLayout & { box: { height: number; width: number } }
replaceColors(replacements: ColorReplacement[]): ColorReplacementResult
Replace colors in the document with new colors Supports bulk replacement operations with multiple color mappings This method modifies the document in place, triggers a redraw, and adds to history
| Parameter | Type | Description |
|---|---|---|
replacements |
ColorReplacement[] |
Array of color replacement mappings |
Returns ColorReplacementResult
Result object with count of replacements made
const result = richText.replaceColors([
{ replaceColor: '#fff', newColor: '#000' },
{ replaceColor: '#333', newColor: '#999' }
]);
console.log(`Replaced ${result.replacedCount} color instances`);
replaceFonts(mappings: ({ from: string; to: string })[], options?: { containerHeight?: number }): void
Replace font families in the document and optionally re-copyfit.
Follows the same atomic history pattern as replaceColors: captures a before snapshot, mutates the document in-place, applies copyfitting when a containerHeight constraint is provided, then captures an after snapshot and pushes a single history entry.
| Parameter | Type | Description |
|---|---|---|
mappings |
({ from: string; to: string })[] |
Array of {from, to} font family replacements |
options |
{ containerHeight?: number } (optional) |
Optional containerHeight for copyfitting after replacement |
replaceVariables(values: readonly VariableDefinition[]): boolean
Merge fields: fill this box's {{id}} markers (any case) with their values, each value in its marker's style
(core/variables.ts, the one rule). Not an undo step. A character a value brings that the box's fonts lack is
fetched as after any edit, and fontsReady waits for it. Returns whether anything was filled.
| Parameter | Type | Description |
|---|---|---|
values |
readonly VariableDefinition[] |
Returns boolean
resize(newPageScaleX: number, newPageScaleY: number): void
Resize this RichText node for a new scale.
This method updates the node's dimensions, position, and document content to match a new page/canvas scale. Unlike setAttrs(), this properly triggers layout recalculation.
If fonts are already loaded, layout is calculated immediately (synchronous). If fonts need to be loaded, the method returns immediately and layout will be calculated automatically when fonts finish loading (triggering a redraw).
| Parameter | Type | Description |
|---|---|---|
newPageScaleX |
number |
The X scale factor for the new canvas/page |
newPageScaleY |
number |
The Y scale factor for the new canvas/page |
// Resize for a canvas that's 2x larger
richText.resize(2.0, 2.0);
layer.draw(); // Will show correctly even if fonts still loading
setCurveAmount(amount: number): void
Set how far the text bends, in degrees of arc, from -360 to 360: positive bends up (the text sits on top of a
circle), negative bends down. It is drawn only while the curve is on (setCurveEnabled).
A box on screen lays itself out and redraws at once. Not an undo step: from a toolbar, use the editing
manager's setCurveAmount, which also turns the curve on and records one.
| Parameter | Type | Description |
|---|---|---|
amount |
number |
box.setCurveAmount(120);
box.setCurveEnabled(true);
setCurveEnabled(enabled: boolean): void
Turn curved text on or off. Turning it on remembers the box's width; turning it off gives the box that width
back. The bend is set with setCurveAmount.
| Parameter | Type | Description |
|---|---|---|
enabled |
boolean |
setDocument(document: RichTextDocument): void
Replace the box's document. When its fonts are already loaded the box is laid out and redrawn
at once; otherwise the missing faces are fetched and it is laid out when they arrive.
node.setAttr("document", doc) and setAttrs({ document }) come through here too.
| Parameter | Type | Description |
|---|---|---|
document |
RichTextDocument |
setWidth(value: number): this
Sets the box's width, as box.width(value) does. The text wraps to the new width.
| Parameter | Type | Description |
|---|---|---|
value |
number |
Returns this
RichTextConfig
interface Plain canvasinterface RichTextConfig extends ShapeConfig
A text box's settings: Konva's shape settings (x, y, width, rotation, draggable, your own attributes…) plus these.
autoHandleTransforms?: boolean
Whether the box handles resizing by a Konva.Transformer itself: a side handle changes the width and the text rewraps; a corner handle scales the text in proportion. Set to false to leave transforms to Konva's default scaling. Default true.
copyfit?: CopyfitConfig
Copyfitting configuration - auto-scale content to fit fixed dimensions. When enabled, text will be scaled to fit within the node's width/height.
document?: RichTextDocument
The text and its formatting. Build one with createDocumentFromText("..."), or pass a document
you saved earlier. Default: an empty document.
documentCurve?: DocumentCurve
Curve state — stored in node.attrs for serialization
editable?: boolean
false: a read-only box - it is selected, moved and resized like any other, but a click never starts editing and
Story Mode does not open on it. Change it later with box.setAttr("editable", false). Default true.
transformer?: Transformer
The Konva.Transformer that resizes this box, when you do not use enableRichText (which tracks your transformer for you).
transformerConfig?: TransformerConfig
Settings for a new Konva.Transformer created for this box, used when transformer is not
given. Read it back with getTransformer() and add it to a layer yourself.
CanvasTextBox
class Konvaclass CanvasTextBox extends BoxApi
A rich text box on a plain HTML canvas: fonts, layout, curved text, shadows, fills, copyfit,
spellcheck and painting, at a place in your scene. Create one with
createCanvasRichText().addBox(); it offers the same box methods as the Konva RichText
(getDocument, setDocument, fontsReady and so on).
A box has a position, a width and a rotation, but never a scale: resizing from a corner scales the type itself (its font sizes), so the text stays sharp.
attrs: Record<string, any>
Serialized attributes (the document, the curve…), as a Konva node's attrs.
copyfitDocument(newDocument: RichTextDocument, options?: { containerHeight?: number; containerWidth?: number; maxIterations?: number; minScale?: number; tolerance?: number }): number
Copyfit a new document into this node by scaling typography to fit within fixed dimensions. The node's x, y, width, and height remain unchanged. The document is scaled to fit. Uses binary search to find the optimal scale factor.
| Parameter | Type | Description |
|---|---|---|
newDocument |
RichTextDocument |
The document to fit into this node |
options |
{ containerHeight?: number; containerWidth?: number; maxIterations?: number; minScale?: number; tolerance?: number } (optional) |
Optional configuration for the copyfitting algorithm |
Returns number
The final scale factor that was applied (1.0 = no scaling, 0.5 = 50% scale, etc.)
const richText = new RichText({ x: 100, y: 100, width: 300, height: 200, document: oldDoc });
const scaleFactor = richText.copyfitDocument(newLargeDoc);
console.log(`Content scaled to ${scaleFactor * 100}% to fit`);
fontsReady(): Promise<void>
Resolves when this node's most recent async initialization (font loading
- initial layout) has finished. Useful for consumers that must rasterize the canvas only after fonts are loaded — e.g. headless PDF/image export. load() starts initialization in the background; await this to know when it's safe to capture the stage.
Never rejects — _initializeAsync handles its own errors internally (transitions to ERROR state) so awaiters get a clean settle either way.
Returns Promise<void>
getCurveAmount(): number
How far the text bends, in degrees of arc: positive bends up, negative bends down, 180 is a half circle.
Default 180. For text set on a circle of a fixed radius (DocumentCurve.arcRadius), the bend worked out
from the text's width.
Returns number
getCurveEnabled(): boolean
Whether the text is drawn on a curve.
Returns boolean
getDocument(): RichTextDocument
The box's document. This is the live object the box draws, not a copy: don't change it in place. To change
the text, edit a copy and pass it to setDocument.
Returns RichTextDocument
The box's current document.
getLastNonEmptyDocument(): null | RichTextDocument
Get the last non-empty document state Useful for undo operations when a text box is deleted
Returns null | RichTextDocument
The last document that contained meaningful content, or null if never had content
getLayout(): TextLayout
The box's current layout — its lines, every letter's position and metrics, its content width and text height, its longest word — as last laid out. Read-only: for hosts that measure what is on the canvas (where the ink is, which colors are drawn). Never lays out.
Returns TextLayout
getLongestWordWidth(): number
The width of the widest single word in the text, in pixels, letter spacing and list indent included: the narrowest the box can be without a word overflowing it. Useful as a minimum width when resizing. Measured with the fonts loaded so far.
Returns number
getMinimumFontSize(): null | number
The smallest font size the text is drawn at, in pixels: each run's own size, or its paragraph's where the run sets none. Useful to keep text legible, for example to stop shrinking at a minimum size.
Returns null | number
The smallest size, or null when the document has no paragraphs.
getUsedColors(): UsedColors
Get all unique colors used in this RichText node's document Returns both text colors and stroke colors as separate arrays All colors are deduplicated and normalized to lowercase
Returns UsedColors
Object containing arrays of unique text colors and stroke colors
getUsedFonts(): string[]
Get all unique font families used in this RichText node's document
Returns string[]
Array of font family names used in the document
height(): number
The text's height (the box sizes itself to its text - at its width as it is now).
Returns number
id(): string
The box's id: the one given in CanvasBoxConfig.id, or one made up for it.
Returns string
layoutAt(doc: RichTextDocument, width: number): TextLayout & { box: { height: number; width: number } }
Lay doc out at width the way this box would — its fonts, its measurer, its fallback size —
and return the result WITHOUT applying it: the box's document, size and lines are untouched.
For trial layouts (copyfit's scales, an app's "does this fit?"). Measures with the fonts already
loaded, as the box itself does.
| Parameter | Type | Description |
|---|---|---|
doc |
RichTextDocument |
|
width |
number |
Returns TextLayout & { box: { height: number; width: number } }
paint(ctx: CanvasRenderingContext2D): void
Paint the box in its own coordinates (the caller has applied its matrix).
| Parameter | Type | Description |
|---|---|---|
ctx |
CanvasRenderingContext2D |
replaceColors(replacements: ColorReplacement[]): ColorReplacementResult
Replace colors in the document with new colors Supports bulk replacement operations with multiple color mappings This method modifies the document in place, triggers a redraw, and adds to history
| Parameter | Type | Description |
|---|---|---|
replacements |
ColorReplacement[] |
Array of color replacement mappings |
Returns ColorReplacementResult
Result object with count of replacements made
const result = richText.replaceColors([
{ replaceColor: '#fff', newColor: '#000' },
{ replaceColor: '#333', newColor: '#999' }
]);
console.log(`Replaced ${result.replacedCount} color instances`);
replaceFonts(mappings: ({ from: string; to: string })[], options?: { containerHeight?: number }): void
Replace font families in the document and optionally re-copyfit.
Follows the same atomic history pattern as replaceColors: captures a before snapshot, mutates the document in-place, applies copyfitting when a containerHeight constraint is provided, then captures an after snapshot and pushes a single history entry.
| Parameter | Type | Description |
|---|---|---|
mappings |
({ from: string; to: string })[] |
Array of {from, to} font family replacements |
options |
{ containerHeight?: number } (optional) |
Optional containerHeight for copyfitting after replacement |
replaceVariables(values: readonly VariableDefinition[]): boolean
Merge fields: fill this box's {{id}} markers (any case) with their values, each value in its marker's style
(core/variables.ts, the one rule). Not an undo step. A character a value brings that the box's fonts lack is
fetched as after any edit, and fontsReady waits for it. Returns whether anything was filled.
| Parameter | Type | Description |
|---|---|---|
values |
readonly VariableDefinition[] |
Returns boolean
rotation(): number
The rotation in degrees, clockwise, about the box's top-left corner.
Returns number
scaleX(): number
Always 1: a canvas box is never scaled (resizing scales its type instead).
Returns number
scaleY(): number
Always 1: a canvas box is never scaled (resizing scales its type instead).
Returns number
setCurveAmount(amount: number): void
Set how far the text bends, in degrees of arc, from -360 to 360: positive bends up (the text sits on top of a
circle), negative bends down. It is drawn only while the curve is on (setCurveEnabled).
A box on screen lays itself out and redraws at once. Not an undo step: from a toolbar, use the editing
manager's setCurveAmount, which also turns the curve on and records one.
| Parameter | Type | Description |
|---|---|---|
amount |
number |
box.setCurveAmount(120);
box.setCurveEnabled(true);
setCurveEnabled(enabled: boolean): void
Turn curved text on or off. Turning it on remembers the box's width; turning it off gives the box that width
back. The bend is set with setCurveAmount.
| Parameter | Type | Description |
|---|---|---|
enabled |
boolean |
setDocument(document: RichTextDocument): void
Replace the box's document. When its fonts are already loaded the box is laid out and redrawn
at once; otherwise the missing faces are fetched and it is laid out when they arrive.
node.setAttr("document", doc) and setAttrs({ document }) come through here too.
| Parameter | Type | Description |
|---|---|---|
document |
RichTextDocument |
setPosition(x: number, y: number): void
Moves the box and asks for a repaint. Not an undo step.
| Parameter | Type | Description |
|---|---|---|
x |
number |
The new left edge, in scene coordinates. |
y |
number |
The new top edge, in scene coordinates. |
setRotation(degrees: number): void
Rotates the box and asks for a repaint. Not an undo step.
| Parameter | Type | Description |
|---|---|---|
degrees |
number |
Clockwise, about the box's top-left corner. |
setWidth(width: number): void
A width DECIDED by someone (the text re-wraps); the box's own layout writes go through the same rule.
| Parameter | Type | Description |
|---|---|---|
width |
number |
width(): number
The width the text wraps at, in scene units.
Returns number
x(): number
The left edge of the box, in scene coordinates.
Returns number
y(): number
The top edge of the box, in scene coordinates.
Returns number
CanvasBoxConfig
interface Konvainterface CanvasBoxConfig
Where a text box sits and how wide it is, in the host's scene coordinates.
copyfit?: CopyfitConfig
Shrink the type to fit a height when the box first loads (see copyfit).
document: RichTextDocument
The text and its formatting: createDocumentFromText("...") for plain text, or a document you
saved earlier (the same format the Konva RichText uses).
documentCurve?: DocumentCurve
Text on an arc (as a saved Konva box carries it): { enabled, amount } in degrees.
editable?: boolean
false: the box is selected and moved like any other, but never edited (attrs.editable later).
id?: string
Optional id (one is made up otherwise): what undo history and events know the box by.
rotation?: number
Degrees, clockwise, about the box's top-left corner.
width: number
The width the text wraps at. The height follows the text.
x: number
The left edge of the box, in scene coordinates.
y: number
The top edge of the box, in scene coordinates.
CopyfitConfig
interface Konva or canvasinterface CopyfitConfig
Configuration for automatic copyfitting behavior. When enabled, text will be scaled to fit within the node's fixed dimensions.
containerHeight?: number
Fixed container height for copyfitting. When set, the algorithm uses this as the vertical constraint instead of the node's auto-calculated height. Essential when text content has been replaced before loading — without this, the auto-calculated height always matches the content and copyfit has no constraint to shrink against.
enabled: boolean
Enable copyfitting for this node
maxFontSize?: number
Maximum allowed font size - caps any font in the document
minFontSize?: number
Minimum allowed font size - stops shrinking if any font hits this
newText?: string
Replacement text for copyfitting pre-modified elements. When set, the node renders its original text/document first (measuring design-intent height), then builds a new document from this text and copyfits it within that height. Each \n-delimited line becomes a paragraph block, inheriting formatting from the original document.
Works for both Konva.Text and RichText source elements — the original document structure provides all formatting; only the text changes.
DocumentCurve
interface Konva or canvasinterface DocumentCurve
A box's curved-text settings, saved with the box (the Konva node's documentCurve attribute). Curved text is
set on one line along a circular arc. Usually changed through setCurveEnabled and setCurveAmount.
amount: number
How far the text bends, in degrees of arc, from -360 to 360: positive bends up (the text sits on top of a
circle), negative bends down (it hangs from the bottom of one). 180 is a half circle. Ignored when
arcRadius is set.
arcRadius?: number
Set the text on a circle of exactly this radius, in pixels: the bend is worked out from the text's own
width, and amount is ignored. For text around a round badge or sticker.
centerX?: number
With arcRadius and centerY: the x of the circle's center, in the box's parent coordinates (the same
space as the box's own x). The box is moved so its arc sits on that circle when it is first laid out;
after that it can be dragged freely.
centerY?: number
With arcRadius and centerX: the y of the circle's center, in the box's parent coordinates.
enabled: boolean
Whether the text is drawn on the curve. While the text is being edited it is drawn flat.
preCurveWidth?: number
The box's width before the curve was turned on, in pixels. The box returns to it when the curve is turned off. Managed by the library.
side?: "top" | "bottom"
With arcRadius: which side of the circle the text runs along. "top" bends up, "bottom" bends down.
Default "top".
Commands
Formatting and editing commands for your toolbar, and the formatting report that keeps it in step. The commands are the same on both surfaces.
TextFormatting
interface Konva or canvasinterface TextFormatting<Box>
The formatting operations, the same on every surface: a toolbar calls exactly these, whether the text is on Konva or on a plain HTML canvas.
Every operation takes the boxes to change, or none for the current box: the box being edited, else the selected box. On the box being edited it changes the selected text, or with only a caret the caret's paragraph; on a box that is not being edited, all of its text. Several boxes change as one undo step. Sizes, outline widths and letter spacing are in pixels as seen: a scaled group's scale is applied.
getFormattingState(box?: null | Box): null | FormattingState
The formatting a toolbar shows: at the caret or selection while editing, else of the first letter.
| Parameter | Type | Description |
|---|---|---|
box |
null | Box (optional) |
Returns null | FormattingState
getText(boxes?: null | Box[]): string
The current box's plain text (the first of boxes), paragraphs joined by newlines.
| Parameter | Type | Description |
|---|---|---|
boxes |
null | Box[] (optional) |
Returns string
scaleFontSize(factor: number, boxes?: null | Box[]): void
The type times factor, in proportion (a headline stays a headline); setFontSize gives one size.
| Parameter | Type | Description |
|---|---|---|
factor |
number |
|
boxes |
null | Box[] (optional) |
setAlignment(alignment: "left" | "center" | "right" | "justify", boxes?: null | Box[]): void
Set the horizontal alignment. It belongs to paragraphs: every paragraph the selection touches changes as a whole.
| Parameter | Type | Description |
|---|---|---|
alignment |
"left" | "center" | "right" | "justify" |
|
boxes |
null | Box[] (optional) |
setBulletStyle(bulletStyle: string, boxes?: null | Box[]): void
Change the marker of the reached list items: "disc", "circle", "square", or any single character.
Paragraphs that are not list items are left as they are.
| Parameter | Type | Description |
|---|---|---|
bulletStyle |
string |
|
boxes |
null | Box[] (optional) |
setColor(color: string, boxes?: null | Box[]): void
Set the text color, as any CSS color string. Also removes an image or gradient fill.
| Parameter | Type | Description |
|---|---|---|
color |
string |
|
boxes |
null | Box[] (optional) |
setCurveAmount(amount: number, boxes?: null | Box[]): void
Bend the text into an arc (degrees; turns the curve on).
| Parameter | Type | Description |
|---|---|---|
amount |
number |
|
boxes |
null | Box[] (optional) |
setDocument(document: RichTextDocument, boxes?: null | Box[]): void
Replace the whole document: one undo step.
| Parameter | Type | Description |
|---|---|---|
document |
RichTextDocument |
|
boxes |
null | Box[] (optional) |
setFill(fill: null | TextFill, boxes?: null | Box[]): void
An image or gradient painted through the letters (null: back to their colors).
| Parameter | Type | Description |
|---|---|---|
fill |
null | TextFill |
|
boxes |
null | Box[] (optional) |
setFontFamily(fontFamily: string, boxes?: null | Box[], options?: FontFamilyOptions): Promise<void>
Change the font family, for example "Georgia". The promise resolves once the font is loaded and the text
is laid out in it; until then the boxes keep their current font. Bold, italic and weight are kept.
Changing several boxes is one undo step. options: a custom font's stylesheet address and details, saved in the
document with the family (see FontFamilyOptions).
| Parameter | Type | Description |
|---|---|---|
fontFamily |
string |
|
boxes |
null | Box[] (optional) |
|
options |
FontFamilyOptions (optional) |
Returns Promise<void>
setFontSize(fontSize: number, boxes?: null | Box[]): void
The size as getFormattingState shows it (a scaled group's text: at the group's scale). Every reached letter takes it.
| Parameter | Type | Description |
|---|---|---|
fontSize |
number |
|
boxes |
null | Box[] (optional) |
setFontWeight(weight: number, boxes?: null | Box[]): void
A numeric weight (100–900): the source of truth over bold.
| Parameter | Type | Description |
|---|---|---|
weight |
number |
|
boxes |
null | Box[] (optional) |
setLetterSpacing(letterSpacing: number, boxes?: null | Box[]): void
Set the extra space between letters, in pixels as seen; 0 is the font's normal spacing. Applies to
whole paragraphs.
| Parameter | Type | Description |
|---|---|---|
letterSpacing |
number |
|
boxes |
null | Box[] (optional) |
setLineHeight(lineHeight: number, boxes?: null | Box[]): void
Set the line height, as a multiple of the font size (1.2 is 120%). Applies to whole paragraphs.
| Parameter | Type | Description |
|---|---|---|
lineHeight |
number |
|
boxes |
null | Box[] (optional) |
setParagraphSpacing(paragraphSpacing: number, boxes?: null | Box[]): void
Space after each paragraph (multiples of its font size).
| Parameter | Type | Description |
|---|---|---|
paragraphSpacing |
number |
|
boxes |
null | Box[] (optional) |
setShadow(shadow: null | TextShadow, boxes?: null | Box[]): void
Set the drop shadow, or remove it with null. The shadow belongs to the whole box, never to a
selection. It changes only how the text is painted, never its layout.
| Parameter | Type | Description |
|---|---|---|
shadow |
null | TextShadow |
|
boxes |
null | Box[] (optional) |
setStrokeColor(color: string, boxes?: null | Box[]): void
Set the outline color, as any CSS color string. Visible only where the outline width is above zero.
| Parameter | Type | Description |
|---|---|---|
color |
string |
|
boxes |
null | Box[] (optional) |
setStrokeWidth(width: number, boxes?: null | Box[]): void
Set the width of the letters' outline, in pixels as seen. 0 removes it. If none of the reached letters
has an outline color yet, black is used.
| Parameter | Type | Description |
|---|---|---|
width |
number |
|
boxes |
null | Box[] (optional) |
setTabStops(tabStops: TabStop[], boxes?: null | Box[]): void
Set the tab stops of whole paragraphs: where the text after each tab sits ({ pos: 1, align: "right", leader: "dots" } parks a price at the right edge behind dots). [] removes them; a tab then goes to the next default stop.
| Parameter | Type | Description |
|---|---|---|
tabStops |
TabStop[] |
|
boxes |
null | Box[] (optional) |
setText(text: string, boxes?: null | Box[]): void
Replace the text, keeping the formatting of the first letter and paragraph.
| Parameter | Type | Description |
|---|---|---|
text |
string |
|
boxes |
null | Box[] (optional) |
toggleBold(boxes?: null | Box[]): void
Turn bold on or off, following what the toolbar shows: if FormattingState.bold is true it is removed, otherwise applied. Also sets the numeric weight to 700 or 400.
| Parameter | Type | Description |
|---|---|---|
boxes |
null | Box[] (optional) |
toggleBulletList(bulletStyle?: string, boxes?: null | Box[]): void
Make the reached paragraphs bulleted list items, or plain paragraphs again: if any is not a list item yet, all become list items; otherwise all lose their bullets.
| Parameter | Type | Description |
|---|---|---|
bulletStyle |
string (optional) |
The marker for new list items: |
boxes |
null | Box[] (optional) |
toggleCurve(boxes?: null | Box[]): void
Curve the box's text along an arc, or straighten it again. Applies to the whole box. Experimental.
| Parameter | Type | Description |
|---|---|---|
boxes |
null | Box[] (optional) |
toggleItalic(boxes?: null | Box[]): void
Turn italic on or off, following what FormattingState.italic shows.
| Parameter | Type | Description |
|---|---|---|
boxes |
null | Box[] (optional) |
toggleStrikethrough(boxes?: null | Box[]): void
Turn strikethrough on or off, following what FormattingState.strikethrough shows.
| Parameter | Type | Description |
|---|---|---|
boxes |
null | Box[] (optional) |
toggleUnderline(boxes?: null | Box[]): void
Turn underline on or off, following what FormattingState.underline shows.
| Parameter | Type | Description |
|---|---|---|
boxes |
null | Box[] (optional) |
RichTextManager
class Plain canvasclass RichTextManager
RichTextManager
Central manager for RichText nodes that handles:
- Edit mode state management
- Mouse & keyboard event handling
- Cursor & selection management
- Transformer setup & management
- Undo/redo operations
- Clipboard operations
This dramatically simplifies library integration by handling all the boilerplate that would otherwise be required in the consuming application.
The caret and the selection are painted over the layer as it draws, not added to it: the library puts no
node of its own in your layers, so stage.toJSON() and your own export see only your design.
For detailed information about event binding structure and troubleshooting, see the Integration Guide documentation.
readonly context: RichTextContext<RichText>
The editor session this manager belongs to (see RichTextManagerConfig.context).
boxes(): RichText[]
The text boxes this manager edits (every RichText on its stage).
Returns RichText[]
cancelStoryMode(): void
Cancel Story Mode without saving changes. Call this from your dialog's Cancel button.
copy(): boolean
Copy selected text to clipboard. This method can be called from consuming applications that want to implement custom copy functionality (e.g., custom context menus, toolbar buttons).
Returns boolean
true if text was copied, false if no selection or not in edit mode
// In a custom context menu handler
menuButton.addEventListener('click', () => {
manager.copy();
});
cut(): boolean
Cut selected text to clipboard. This method can be called from consuming applications that want to implement custom cut functionality (e.g., custom context menus, toolbar buttons).
Returns boolean
true if text was cut, false if no selection or not in edit mode
// In a custom context menu handler
menuButton.addEventListener('click', () => {
manager.cut();
});
destroy(): void
Stop managing everything: leaves editing, and removes exactly what this manager added — its stage and box handlers (namespaced ".richtextManager"; the host's own handlers stay), its window keyboard/paste listeners, and its boxes from the context's map and editing state. After this, nothing reacts to keys or clicks on its behalf.
edit(richText: RichText): void
Start editing richText now, as a tap on it while selected would: it becomes the selection and takes
the caret. For a host gesture that should go straight to typing (a double-click on a decorated group
whose words are the point). A box marked editable: false declines, exactly as it declines a tap.
| Parameter | Type | Description |
|---|---|---|
richText |
RichText |
editing(): null | RichText
The text box being edited, or null.
Returns null | RichText
enterStoryMode(box?: RichText): void
Opens Story Mode - a larger editor for long text, over the canvas - on box, else on the box being edited,
else on the selected box. Inside your own panel after setStoryModeContainer. While it is open, formatting
commands reach the text selected in the panel. A box marked editable: false doesn't open.
| Parameter | Type | Description |
|---|---|---|
box |
RichText (optional) |
The box to open it on (one of this manager's). Default: the box being edited, else the selected one. |
exitStoryMode(): null | RichText
Exit Story Mode and save changes. Call this from your dialog's Apply/Save button.
Returns null | RichText
The updated RichText node, or null if validation failed
hasSelection(): boolean
Check if there is currently a text selection. Useful for determining whether copy/cut operations are available.
Returns boolean
true if there is a selection, false otherwise
isStoryModeActive(): boolean
Check if Story Mode is currently active
Returns boolean
paste(): Promise<void>
Paste content from clipboard into the currently editing RichText node. This method should be called from consuming applications that want to implement custom paste functionality (e.g., custom context menus, toolbar buttons).
Note: This method must be called during a user gesture (e.g., button click) to have clipboard access. If called outside a user gesture context, clipboard access may be denied by the browser.
Returns Promise<void>
Promise that resolves when paste is complete
// In a custom context menu handler
menuButton.addEventListener('click', async () => {
await manager.paste();
});
redo(): void
Redo the last undone operation
replaceVariables(values: readonly VariableDefinition[], boxes?: readonly RichText[]): void
Merge fields: fill {{id}} markers (any case) with their values, each value in its marker's style - in every box
this manager edits, or only boxes (one name on every surface: a box's own replaceVariables, the canvas's
rt.replaceVariables). Not an undo step.
| Parameter | Type | Description |
|---|---|---|
values |
readonly VariableDefinition[] |
|
boxes |
readonly RichText[] (optional) |
selected(): null | RichText
The selected text box on this stage, or null (TextFormatting's "current box" when none is being edited).
Returns null | RichText
setStoryModeContainer(container: undefined | HTMLElement): void
Set the container for Story Mode. Call this before enterStoryMode() with your dialog's content div.
| Parameter | Type | Description |
|---|---|---|
container |
undefined | HTMLElement |
The container element, or undefined to use document.body |
// User clicks button to open dialog
const dialog = createMyDialog();
const editorDiv = dialog.querySelector('#story-editor-container');
manager.setStoryModeContainer(editorDiv);
manager.enterStoryMode();
stopEditing(): void
Stop editing (the caret and selection go; the box stays selected).
undo(): void
Undo the last operation
Plus every command in TextFormatting, described there: getFormattingState, getText, scaleFontSize, setAlignment, setBulletStyle, setColor, setCurveAmount, setDocument, setFill, setFontFamily, setFontSize, setFontWeight, setLetterSpacing, setLineHeight, setParagraphSpacing, setShadow, setStrokeColor, setStrokeWidth, setTabStops, setText, toggleBold, toggleBulletList, toggleCurve, toggleItalic, toggleStrikethrough, toggleUnderline.
CanvasRichText
interface Konvainterface CanvasRichText extends TextFormatting<CanvasTextBox>
Rich text on a plain HTML canvas, as returned by createCanvasRichText: the text boxes,
the editor, and every formatting command (toggleBold, setFontSize, setColor and the rest).
Your app forwards pointer events to it (pointerDown, pointerMove, pointerUp, click,
contextMenu) and calls paint and paintOverlay from its own render. Keyboard input and paste
are heard on the window while a box is being edited.
addBox(config: CanvasBoxConfig): CanvasTextBox
Add a text box to the scene.
| Parameter | Type | Description |
|---|---|---|
config |
CanvasBoxConfig |
Returns CanvasTextBox
boxes(): readonly CanvasTextBox[]
The boxes, bottom to top (the order paint draws them in).
Returns readonly CanvasTextBox[]
cancelStoryMode(): void
Closes Story Mode and puts the box's text back as it was.
click(p: CanvasPointer): { handled: boolean }
The click the browser sends after a press and release on the same spot.
| Parameter | Type | Description |
|---|---|---|
p |
CanvasPointer |
Returns { handled: boolean }
contextMenu(p: CanvasPointer, screen: { x: number; y: number }): { handled: boolean }
The browser's contextmenu event (a right-click): on the box being edited, the library's menu opens at
screen (the event's clientX / clientY) - spelling suggestions for a misspelled word, then Cut / Copy /
Paste. handled: it opened (call preventDefault() on the event so the browser's own menu doesn't).
| Parameter | Type | Description |
|---|---|---|
p |
CanvasPointer |
|
screen |
{ x: number; y: number } |
Returns { handled: boolean }
copy(): boolean
Copy the selected text to the clipboard. Returns whether there was a selection.
Returns boolean
cursor(): string
The mouse cursor at the last pointer event (a CSS cursor name) - the same rule on every surface. Apply it
after each pointer call: canvas.style.cursor = rt.cursor().
Returns string
cut(): boolean
Cut the selected text to the clipboard (an undo step). Returns whether there was a selection.
Returns boolean
destroy(): void
Stop listening and release every box.
editing(): null | CanvasTextBox
The box being edited, or null.
Returns null | CanvasTextBox
enterStoryMode(box?: CanvasTextBox): void
Opens Story Mode - a larger editor for long text, over the canvas - on box, else on the box being edited, else
on the selected box. The overlay opens over it when storyModePlacement says where it is, centered otherwise; or
inside your own panel after setStoryModeContainer. While it is open, formatting commands reach the text
selected in the panel. A box marked editable: false doesn't open.
| Parameter | Type | Description |
|---|---|---|
box |
CanvasTextBox (optional) |
The box to open it on. Default: the box being edited, else the selected one. |
exitStoryMode(): null | CanvasTextBox
Closes Story Mode and writes its text to the box (one undo step). Returns the box, or null.
Returns null | CanvasTextBox
fromEvent(e: MouseEvent, canvas: HTMLCanvasElement, sceneToCanvas?: DOMMatrix): CanvasPointer
A DOM pointer event as a CanvasPointer: canvas pixels, then (optional) the host's scene matrix inverted.
| Parameter | Type | Description |
|---|---|---|
e |
MouseEvent |
|
canvas |
HTMLCanvasElement |
|
sceneToCanvas |
DOMMatrix (optional) |
Returns CanvasPointer
hasSelection(): boolean
Is text selected in the box being edited? (Whether copy and cut can do anything.)
Returns boolean
hitTest(p: { x: number; y: number }): null | CanvasTextBox
The topmost box under a scene point, or null.
| Parameter | Type | Description |
|---|---|---|
p |
{ x: number; y: number } |
Returns null | CanvasTextBox
isStoryModeActive(): boolean
Whether Story Mode is open.
Returns boolean
onFormattingChange(callback: (formatting: null | FormattingState, box: null | CanvasTextBox) => void): () => void
The formatting at the caret (while editing) or of the selected box, whenever it changes: a toolbar follows it. Called at once with the current state. Returns the unsubscribe.
| Parameter | Type | Description |
|---|---|---|
callback |
(formatting: null | FormattingState, box: null | CanvasTextBox) => void |
Returns () => void
paint(ctx: CanvasRenderingContext2D): void
Paint every box, each under its own matrix, on top of the context's current transform.
| Parameter | Type | Description |
|---|---|---|
ctx |
CanvasRenderingContext2D |
paintOverlay(ctx: CanvasRenderingContext2D): void
Paint the caret and the selection of the box being edited (call after everything else).
| Parameter | Type | Description |
|---|---|---|
ctx |
CanvasRenderingContext2D |
paste(): Promise<boolean>
Paste the clipboard at the caret (call from a user gesture). Resolves false when no box is being edited.
Returns Promise<boolean>
pointerDown(p: CanvasPointer): { handled: boolean }
Pointer input. handled: the text editor took the event (else it is the host's).
| Parameter | Type | Description |
|---|---|---|
p |
CanvasPointer |
Returns { handled: boolean }
pointerMove(p: CanvasPointer): { handled: boolean }
Pointer movement: drag-selecting text, moving or resizing a box with the object controls, and
hover feedback. handled: the library used the event (else it is the host's).
| Parameter | Type | Description |
|---|---|---|
p |
CanvasPointer |
Returns { handled: boolean }
pointerUp(p: CanvasPointer): { handled: boolean }
Pointer release: ends a drag-select, move or resize. handled: the library used the event.
| Parameter | Type | Description |
|---|---|---|
p |
CanvasPointer |
Returns { handled: boolean }
ready(): Promise<void>
Every box on its real fonts and laid out - what to await before exporting or measuring (Konva: richTextReady). Bounded by the font time limit, so a font that never arrives can't hold it forever.
Returns Promise<void>
redo(): void
Redoes the last undone change. While a box is being edited, Ctrl+Y (Cmd+Y on Mac) does the same.
removeBox(box: CanvasTextBox): void
Take a box out of the scene.
| Parameter | Type | Description |
|---|---|---|
box |
CanvasTextBox |
replaceVariables(variables: readonly VariableDefinition[], boxes?: readonly CanvasTextBox[]): void
Merge fields: fill {{id}} markers (any case) with their values, each value in its marker's style - in every
box, or only boxes. Not an undo step. (One name on every surface: a box's own, Konva's manager.replaceVariables.)
| Parameter | Type | Description |
|---|---|---|
variables |
readonly VariableDefinition[] |
|
boxes |
readonly CanvasTextBox[] (optional) |
restore(step: HistoryStep, which: "before" | "after"): number
"host" history: undo ("before") or redo ("after") a step your app kept from onHistoryStep - every box it
covers goes back to that state, as the library's own undo would put it. Returns how many boxes were restored.
| Parameter | Type | Description |
|---|---|---|
step |
HistoryStep |
|
which |
"before" | "after" |
Returns number
select(box: null | CanvasTextBox): void
Select a box (or none). A selected box's next click edits it.
| Parameter | Type | Description |
|---|---|---|
box |
null | CanvasTextBox |
selected(): null | CanvasTextBox
The selected box, or null.
Returns null | CanvasTextBox
setStoryModeContainer(container: undefined | HTMLElement): void
Render Story Mode inside your own element (a panel or dialog) instead of an overlay; undefined: the overlay.
| Parameter | Type | Description |
|---|---|---|
container |
undefined | HTMLElement |
stopEditing(): void
Leave edit mode.
undo(): void
Undoes the last text change (typing, formatting, a move or resize). The boxes it changes are restored and selected; edit mode stays as it was. While a box is being edited, Ctrl+Z (Cmd+Z on Mac) does the same.
Plus every command in TextFormatting, described there: getFormattingState, getText, scaleFontSize, setAlignment, setBulletStyle, setColor, setCurveAmount, setDocument, setFill, setFontFamily, setFontSize, setFontWeight, setLetterSpacing, setLineHeight, setParagraphSpacing, setShadow, setStrokeColor, setStrokeWidth, setTabStops, setText, toggleBold, toggleBulletList, toggleCurve, toggleItalic, toggleStrikethrough, toggleUnderline.
FormattingState
interface Konva or canvasinterface FormattingState
Formatting state at the current cursor/selection position. This is emitted whenever the selection changes to keep UI in sync.
alignment: "left" | "center" | "right" | "justify"
The paragraph's alignment.
bold: boolean
The text is bold.
canRedo: boolean
Redo can step forward.
canUndo: boolean
Undo can step back.
color: string
The color, as CSS.
cursorIndex: number
The caret's position, as a letter index in the box.
fontFamily: string
The font family.
fontSize: number
The size in pixels, as it looks on screen (a scaled group's scale applied).
fontWeight?: number
Numeric font weight of the selection (100–900), when uniform. Optional and
READ-ONLY reporting — it reflects the document's existing fontWeight/bold
attributes for the toolbar's weight picker; it does not affect rendering.
Undefined when not resolvable (e.g. mixed weights across the selection).
hasSelection: boolean
Text is selected (not just a caret).
isList: boolean
The paragraph is a list item.
italic: boolean
The text is italic.
letterSpacing: number
The paragraph's letter spacing in pixels, as it looks on screen.
lineHeight: number
The paragraph's line height, as a multiple of the font size.
listStyle?: string
The list's marker.
listType?: "bullet"
The list's kind, when it is a bulleted list.
paragraphSpacing: number
The space after the paragraph, as a multiple of its font size.
selectionLength: number
How many letters are selected.
strikethrough: boolean
The text is struck through.
strokeColor: string
The outline's color.
strokeWidth: number
The outline's width in pixels, as it looks on screen (0: none).
tabStops: TabStop[]
The paragraph's tab stops; empty when it has none (a tab then goes to the next default stop).
underline: boolean
The text is underlined.
FontFamilyOptions
interface Plain canvasinterface FontFamilyOptions
A custom font's details for RichTextManager.setFontFamily, saved in the document with the family.
The descriptive fields (fontImageUrl, description, attribution) are saved only for a custom font:
one given a fontFaceUrl here, or one your fonts lookup knows.
attribution?: string
The font's author or license credit. Stored only.
description?: string
A short, user-friendly description of the font. Stored only.
fontFaceUrl?: string
The address of a stylesheet with the font's @font-face rules. Used to load the font, and saved so the design opens anywhere.
fontImageUrl?: string
The address of a preview image of the font, for a font picker. Stored only; never drawn.
Documents
The JSON a text box holds, and the functions that make and change it.
RichTextDocument
interface Konva or canvasinterface RichTextDocument
What a text box holds: its paragraphs (blocks) and the box-wide settings - a fill painted through the letters, a
drop shadow, where its custom fonts live. Plain JSON: save it with your design and load it anywhere; it draws the
same in every browser and in print.
baseFontSize?: number
The size, in pixels, of new text with no size of its own (an empty box's caret, a new paragraph).
blocks: Block[]
The paragraphs, in order.
fill?: TextFill
A gradient or image that fills every letter in the box, in place of the text colors. Setting a solid color
(setColor) removes it.
fontMetadata?: FontMetadata[]
Where the document's custom fonts live, recorded as fonts are applied, so the design opens anywhere without your font lookup.
renderVersion?: number
The text version this document renders with (core/rendering.ts). None = version 1: made before versions existed. Saved with the text, so it renders the same wherever it goes.
shadow?: TextShadow
A drop shadow behind all of the box's text. No shadow when absent.
Block
interface Konva or canvasinterface Block
One paragraph of a document: a plain paragraph or a list item, its look, and its text in styled runs.
blockAttributes: BlockAttributes
The paragraph's look and layout.
content: InlineContent[]
The paragraph's text, in runs of one style each. A paragraph always has at least one run (an empty one: text: "").
id: string
A unique id within the document.
type: "paragraph" | "list-item"
"paragraph", or "list-item" for a line with a bullet or number.
BlockAttributes
interface Konva or canvasinterface BlockAttributes
A paragraph's look: the size, family and color its letters default to, and the paragraph's own layout - alignment, line height, letter spacing, spacing after it - plus an optional weight, all caps, background, border, tab stops and list marker.
alignment: "left" | "center" | "right" | "justify"
How the paragraph's lines sit in the box.
allCaps?: boolean
Display the paragraph in capitals. The words stay as typed — only layout and paint see capitals
(see displayText), so copy, paste, spellcheck and switching it off all get the real text back.
Set it for headings, labels and other text shown in capitals.
backgroundColor?: string
A color filled behind the paragraph's lines.
borderColor?: string
The color of a border around the paragraph.
borderWidth?: number
The width of that border, in pixels.
bulletOffset?: number
For a list item: how far its marker sits from the left, as a multiple of its font size (default 0.6).
color: string
The color of the paragraph's letters, as CSS: "#5e2dc1", "rgb(94, 45, 193)".
fontFamily: string
The font family of the paragraph's letters, e.g. "Lora" (a run may set its own).
fontSize: number
The size of the paragraph's letters in pixels (a run may set its own).
fontWeight?: number
A numeric weight for the paragraph's letters, 100-900 (a run may set its own).
letterSpacing: number
Extra space after every letter, in pixels (0 is none; negative tightens).
lineHeight: number
The height of a line as a multiple of the font size: 1.2 is normal.
listIndent?: number
For a list item: how far its text sits from the left, as a multiple of its font size (default 2).
listStyle?: string
For a list item: the marker - "disc", "circle", "square", "decimal", or any character, e.g. "➤".
listType?: "number" | "bullet"
For a list item: "bullet" (the default) or "number".
marker?: ListMarker
Vector list marker, drawn instead of the listStyle character.
A marker is TWO shapes in TWO colors — a white check inside a filled circle, a digit on a rounded square — so a single path plus a single color cannot express the common cases. Omit it and the character path is used, unchanged.
paragraphSpacing: number
Space after the paragraph, as a multiple of its font size (0 is none).
tabStops?: TabStop[]
Tab stops for this block. A \t in the text advances to the next stop.
An ARRAY from the outset even though v1 ships a single right stop: a user-pressed
Tab key is a stated direction, and Item ⇥ Qty ⇥ Price is the obvious first thing
someone does with it. Widening this later would be a document migration; starting
wide costs nothing.
InlineContent
interface Konva or canvasinterface InlineContent
A run of text in one style. Its attributes override the paragraph's look for these letters only; a soft line break
(Shift+Enter) before or after the run is recorded beside its text.
attributes: InlineAttributes
The run's own style, over its paragraph's.
startsWithSoftBreak?: boolean
The run starts on a new line within its paragraph (a soft break, Shift+Enter).
text: string
The run's letters.
trailingSoftBreakCount?: number
How many soft breaks follow the run's text (empty lines within the paragraph).
InlineAttributes
interface Konva or canvasinterface InlineAttributes
A run's own style, over its paragraph's: anything left out comes from the paragraph.
bold?: boolean
Bold (weight 700, unless fontWeight says otherwise).
color?: string
The run's color, as CSS, over its paragraph's.
fontFamily?: string
The run's font family, over its paragraph's.
fontSize?: number
The run's size in pixels, over its paragraph's.
fontWeight?: number
A numeric weight, 100-900. Wins over bold.
italic?: boolean
Italic.
strikethrough?: boolean
Struck through.
strokeColor?: string
The color of an outline around the letters.
strokeWidth?: number
The width of that outline, in pixels (0 or absent: none).
underline?: boolean
Underlined.
ListMarker
interface Konva or canvasinterface ListMarker
A list marker as vector art rather than a text glyph.
Lives on BlockAttributes so it serializes, clones and scales with the document for
free. The library deliberately knows nothing about named icon sets or presets — it
receives geometry and paints it.
glyphColor?: string
Glyph color. Defaults to the text color, matching the character-bullet behavior.
glyphPath?: string
SVG path data for the glyph, in a 24x24 viewBox. Omit for a shape-only marker.
scale?: number
Marker box as a multiple of fontSize. Default 1.0.
shape?: "none" | "square" | "circle"
Backing shape painted behind the glyph. Omit or "none" for a bare glyph.
shapeFill?: string
Fill for the backing shape. Ignored when there is no shape.
TabStop
interface Konva or canvasinterface TabStop
One tab stop, in the paragraph-attribute model Word and InDesign use — a ruler position belonging to the block, not a one-off jump belonging to a character.
align: "left" | "center" | "right"
How the run after the tab sits against the stop.
v1 IMPLEMENTS "right" only; "left" and "center" are accepted and fall back to it, so a document authored by a later version stays readable by this renderer instead of throwing. Declared as a union rather than a boolean because widening a union is free and changing a boolean's shape is a migration.
leader?: "dots" | "rule"
Optional leader painted across the gap.
pos: number
Position as a fraction of the node width. 1.0 is the right edge.
TextFill
type Konva or canvastype TextFill = ImageFill | GradientFill
A non-color fill source for text glyphs (overrides per-run color).
GradientFill
interface Konva or canvasinterface GradientFill
Paints the text's letters with a two-color linear gradient across the whole box, instead of their colors.
Set it as the document's fill.
doc.fill = { type: "gradient", color1: "#5e2dc1", color2: "#ff7903", angle: 90 }; // left to right
angle: number
The gradient's direction in degrees, as in CSS linear-gradient: 0 runs from color1 at the bottom to
color2 at the top, 90 from left to right, 180 from top to bottom.
color1: string
The color the gradient starts with, any CSS color.
color2: string
The color the gradient ends with, any CSS color.
type: "gradient"
Always "gradient".
ImageFill
interface Konva or canvasinterface ImageFill
Paints the text's letters with an image instead of their colors. Set it as the document's fill.
doc.fill = { type: "image", url: "https://example.com/wood.jpg", fit: "cover" };
fit?: "cover" | "contain" | "stretch"
How the image maps onto the text box. Defaults to "cover".
type: "image"
Always "image".
url: string
The image's URL. It must be loadable by the page (served with CORS when it is on another origin).
TextShadow
interface Konva or canvasinterface TextShadow
Document-level drop shadow. A single shadow (not a CSS shadow list).
color may be rgba() — its alpha carries the shadow opacity. blur is in
px with CSS text-shadow blur-radius semantics (stored raw; the renderer
maps it to a Gaussian σ = blur/2).
blur: number
Blur radius in pixels, as in CSS text-shadow. 0 is a hard-edged shadow.
color: string
Shadow color, any CSS color. Use rgba() to make it translucent, e.g. "rgba(0, 0, 0, 0.4)".
offsetX: number
Horizontal offset in pixels; positive moves the shadow right.
offsetY: number
Vertical offset in pixels; positive moves the shadow down.
FontMetadata
interface Konva or canvasinterface FontMetadata
Where a font the document uses comes from, saved with the document (its fontMetadata) so the text finds the
same font wherever it is opened. Only source: "custom" entries change how a font is loaded: their files or
fontFaceUrl are used before any other source.
doc.fontMetadata = [{ family: "Acme Sans", source: "custom", fontFaceUrl: "https://cdn.example.com/acme.css" }];
attribution?: string
The font's author or license notice. Informational.
description?: string
A description for people choosing a font. Informational.
family: string
The font family name, exactly as the document's fontFamily attributes spell it.
files?: FontFileAddress[]
A custom font's files, one per face (weight and style). Used instead of fontFaceUrl when both are given.
fontFaceUrl?: string
A custom font's stylesheet: the URL of a CSS file with @font-face rules for the family.
fontImageUrl?: string
A preview image of the font, for font pickers. Not used to draw text.
metadata?: Record<string, any>
Any other data you want saved with the font. The library does not read it.
source: "google" | "custom" | "system"
Where the font comes from: "google" (Google Fonts), "custom" (your own files or stylesheet, given here) or "system" (installed on the device).
variants?: string[]
The faces the family offers, for font pickers, e.g. ["400", "700", "400italic"]. Informational.
weights?: number[]
The weights a custom font ships, e.g. [400, 700]. Recorded with a stylesheet font so the design draws the same weights where it opens without your fonts lookup (a weight it lacks resolves to one it has, never a fake bold). Files carry their own weights. Version 2 text reads it.
FontFileAddress
interface Konva or canvasinterface FontFileAddress
One face of a custom font, by address (what a saved design records).
style: "normal" | "italic"
The face's style.
url: string
A .woff2, .woff, .ttf or .otf file.
weight: number
The face's weight, 100 to 900 (400 is regular, 700 bold).
createDocumentFromText
function Konva or canvascreateDocumentFromText(text: string, fontSize?: number, fontFamily?: string, color?: string): RichTextDocument
A document from plain text: one paragraph per line (\n), every paragraph in the same style. Blank lines
between paragraphs are kept as empty paragraphs; blank lines at the end are dropped.
| Parameter | Type | Description |
|---|---|---|
text |
string |
The text, one paragraph per line. |
fontSize |
number (optional) |
Font size in pixels. Default 16. |
fontFamily |
string (optional) |
Font family name. Default Poppins. |
color |
string (optional) |
Text color, any CSS color. Default black. |
Returns RichTextDocument
A new document.
const doc = createDocumentFromText("Grand opening\nThis Saturday", 32, "Lato", "#5e2dc1");
replaceVariablesInDocument
function Konva or canvasreplaceVariablesInDocument(document: RichTextDocument, variables: readonly VariableDefinition[]): RichTextDocument
The document with every marker filled - a new document; document itself is never changed. Nothing to fill:
document is handed back as it is (callers can tell nothing changed by identity).
| Parameter | Type | Description |
|---|---|---|
document |
RichTextDocument |
|
variables |
readonly VariableDefinition[] |
Returns RichTextDocument
VariableDefinition
interface Konva or canvasinterface VariableDefinition
A merge field and the value it is filled with.
id: string
The field's name, as written between the braces (e.g. "phone" for {{phone}}).
value: undefined | null | string
The text it becomes. Written as is (a "$" is a dollar sign, never a replacement pattern). None (null): empty text.
scaleDocumentTypography
function Plain canvasscaleDocumentTypography(document: RichTextDocument, scaleFactor: number): RichTextDocument
Scale all typography properties in a document by a given factor. Used for copyfitting - scaling content to fit within fixed dimensions.
| Parameter | Type | Description |
|---|---|---|
document |
RichTextDocument |
The document to scale |
scaleFactor |
number |
The factor to scale by (e.g., 0.5 = 50% of original size) |
Returns RichTextDocument
A new scaled document (does not mutate the original)
UsedColors
interface Konva or canvasinterface UsedColors
Result interface for color extraction
shadowColors: string[]
The drop shadow's color (its opacity kept: "rgba(0, 0, 0, 0.4)").
strokeColors: string[]
The outline (stroke) colors drawn, each once; a stroke color with no stroke width is not drawn and not listed.
textColors: string[]
The letters' colors, each once: run and paragraph colors (or a gradient fill's two colors, which paint over them) and list marker colors. Opaque colors are given as "#rrggbb", translucent ones as "rgba(r, g, b, a)".
ColorReplacement
interface Konva or canvasinterface ColorReplacement
Interface for color replacement operations
newColor: string
The color to use instead, any CSS color. Each place keeps its own opacity.
replaceColor: string
The color to replace, in any CSS spelling ("#FFF", "white" and "rgb(255, 255, 255)" all match the same color). It matches that color at any opacity.
ColorReplacementResult
interface Konva or canvasinterface ColorReplacementResult
Result interface for color replacement operations
replacedCount: number
How many places changed color in all: the sum of the three counts below.
replacements: ({ count: number; newColor: string; oldColor: string })[]
Each replacement that changed something, with the color it matched (oldColor, as "#rrggbb"), the color
it put there and how many places it changed. Replacements that matched nothing are left out.
shadowColorReplacements: number
How many shadow colors changed (0 or 1: a document has at most one shadow).
strokeColorReplacements: number
How many outline (stroke) colors changed.
textColorReplacements: number
How many letter colors changed (runs, paragraphs, gradient colors and list markers).
Fonts, layout and rendering
Loading fonts, waiting for them, laying text out without a box, and how text is drawn.
loadFonts
function Konva or canvasloadFonts(requests: RichTextDocument | FontRequest[]): Promise<FontLoadResult>
Load fonts — a font picker's previews, a font about to be applied. The ONE font loader: every face a text box draws or measures comes through the same place, so nothing is fetched twice, and each face comes out ready to draw AND to measure.
await loadFonts(["Lato", "Bitter"]); // ready for regular / bold / italic text loadFonts([{ family: "Lato", weights: [300, 900] }]); // exactly those weights (fire and forget)
await loadFonts(document); // everything a document uses, as a box would
Never throws: families that could not load are in failed. Text boxes load what they use on
their own; this is only for fonts not (yet) in a text box — a picker's previews, or a document
about to be measured with layoutText.
| Parameter | Type | Description |
|---|---|---|
requests |
RichTextDocument | FontRequest[] |
Returns Promise<FontLoadResult>
FontRequest
type Konva or canvastype FontRequest = string | { family: string; italic?: boolean; weights: number[] }
A family to load: its name (regular, bold, italic, bold italic), or specific weights.
FontLoadResult
interface Konva or canvasinterface FontLoadResult
Per FAMILY: loaded = at least one requested face is drawable; failed = none could be loaded.
allSuccess: boolean
True when every family can be drawn. A family still loading when the wait ends is in neither list, and makes this false.
error?: Error
The error that stopped loading, when one did. Usually absent: a failed family is listed in failed.
failed: string[]
The families that cannot be drawn: none of the faces asked for loaded. Text in them uses a stand-in font.
loaded: string[]
The families that can be drawn: at least one of the faces asked for loaded.
FontLookup
type Konva or canvastype FontLookup = (family: string) => FontInfo | null | undefined
Your app's answer for a font family it knows about (configureRichText fonts.lookup): where its files or stylesheet
live and which weights it ships. Return nothing for a family you know nothing about (a Google family works without).
FontInfo
interface Konva or canvasinterface FontInfo
What the app knows about one family. Every field optional.
files?: FontFile[]
The family's font files, one per face. The weights it ships are these files' weights (a weight it
lacks is drawn from the nearest file, never synthesized). Takes precedence over fontFaceUrl.
fontFaceUrl?: string
A stylesheet with the family's @font-face rules (a brand or uploaded font).
weights?: number[]
The weights the family ships, so a weight it lacks resolves to one it has (no fake bold).
FontFile
interface Konva or canvasinterface FontFile
One face of a family, as a font file: its address or its bytes. Any of .woff2, .woff, .ttf, .otf. The library draws and measures from this one file.
bytes?: ArrayBuffer
The file itself: an upload, a font bundled with the app, offline use. Not recorded in designs.
style?: "normal" | "italic"
Default "normal".
url?: string
Where the file is (it must allow cross-origin requests when on another origin).
weight?: number
The weight this file draws, 100-900. Default 400.
FontFaceRequest
interface Konva or canvasinterface FontFaceRequest
One font face the library needs. weight is already resolved to one the family ships.
family: string
The family asked for.
style: "normal" | "italic"
The style asked for.
weight: number
The weight asked for, 100-900.
FontError
interface Konva or canvasinterface FontError
A font face that could not be loaded. The text keeps drawing in a fallback font meanwhile.
cors-blocked: the font is on another origin whose server does not allow cross-origin reads. The library reads font files itself (to measure them), so the font host must sendAccess-Control-Allow-Origin(e.g.*or your app's origin). Not tried again. A server error on a host that leaves that header off its error pages looks the same from the page, and is reported as this too.css-fetch-failed/font-fetch-failed: the stylesheet / the file could not be fetched (transient: a network or server error, tried again later; otherwise the address has nothing, e.g. a 404).no-matching-rule: the stylesheet has no rule for this face.font-parse-failed: the file is not a font the library can read (.woff2, .woff, .ttf, .otf).
family: string
The family that could not be loaded.
reason: "cors-blocked" | "css-fetch-failed" | "no-matching-rule" | "font-fetch-failed" | "font-parse-failed" | "font-draw-failed"
Why. Every reason but the last is about getting the file; font-draw-failed: the file loaded, but the browser would not draw it.
style: "normal" | "italic"
The face's style.
transient: boolean
Worth trying again later (the library does, by itself).
url?: string
The address that failed, when there is one.
weight: number
The face's weight.
FallbackFontRequest
interface Konva or canvasinterface FallbackFontRequest
One fallback-font download: a Noto family, a style, and the exact characters that are missing.
chars: string
The characters needed: a stylesheet may serve only these (a subset).
family: string
The fallback family, e.g. "Noto Sans JP".
hasItalicAxis: boolean
The family has an italic axis (ask for italic in the address).
hasWeightAxis: boolean
Whether the family is served with a weight / italic axis (Google's css2 needs to know).
italic: boolean
Whether that text is italic.
weight: number
The weight to match the text it stands in for.
EmojiSource
interface Konva or canvasinterface EmojiSource
Where emoji images come from (vector mode): a base address serving one SVG per emoji, named by its code points.
Default: our hosted Noto emoji set. loadImage turns an SVG's text into something a canvas can draw, for
environments without Image (a server).
baseUrl: string
Folder serving emoji_u
loadImage?: (svgText: string) => Promise<CanvasImageSource>
How to turn an SVG file into something drawImage takes. Defaults to the browser's.
richTextReady
function Plain canvasrichTextReady(container: Container): Promise<void>
Wait until every text box inside container (a stage, layer or group — nested groups too, and
copies no editor manages, e.g. a thumbnail clone) is laid out on its real fonts. The one wait an
app needs before printing, taking a picture, or saying "loaded". A box given a new document while
waiting is waited for again; a font that can't load doesn't hang it (each box's own time limit).
Never rejects.
| Parameter | Type | Description |
|---|---|---|
container |
Container |
Returns Promise<void>
layoutText
function Konva or canvaslayoutText(document: RichTextDocument, options: { width: number }): TextLayout
Lay a document out at width without a text box — the same layout a box performs on its first
layout (same normalization, wrapping, measuring), returned instead of applied. For finishing
documents before they reach a canvas (a converter fitting text to its source's line breaks) and for
trial layouts (does it fit at this size?). Synchronous and side-effect free: it measures with the
fonts already loaded — await loadFonts(doc) first for exact results.
| Parameter | Type | Description |
|---|---|---|
document |
RichTextDocument |
|
options |
{ width: number } |
Returns TextLayout
TextLayout
interface Konva or canvasinterface TextLayout
What layoutText returns: a document laid out at a width, with no box.
contentWidth: number
The widest line (a box this wide shows every line unwrapped by it).
graphemes: GraphemeMetric[]
Every grapheme's position and metrics, as a box's layout produces them.
height: number
The height the text needs (a box sizes itself to this).
lines: LaidOutLine[]
The lines, as a box's getLines() reports them.
longestWordWidth: number
The widest single word (for a list, plus its indent): narrower than this, a word overflows.
LaidOutLine
interface Konva or canvasinterface LaidOutLine
One line exactly as the layout set it — the same lines the renderer draws.
Exposed because the line count is part of a design: a headline stacked on two lines by WRAPPING and rewritten to copy that sets on one leaves the second line's band empty. Nothing outside the layout can know where the wraps fell — an estimate from height and font size cannot tell a mixed-size line from two lines — so the layout reports it.
baselineY: number
Baseline, in the node's own coordinates. One per line, whatever sizes the line mixes.
blank: boolean
No visible characters — an empty paragraph kept for spacing.
blockId: string
The block (paragraph) this line belongs to. A wrapped paragraph spans several lines.
endsWith: "paragraph" | "wrap" | "softbreak" | "end"
Why the line ended: the text ran out of room (wrap), the paragraph ended (paragraph),
an authored in-paragraph break (softbreak), or it is the last line (end).
text: string
The line's words, joined as laid out, without the whitespace a wrap swallowed.
width: number
Visible extent, list indent and tab columns included — what maxLineWidth is the max of.
GraphemeMetric
interface Konva or canvasinterface GraphemeMetric
One laid-out letter: what it is, its style, and where it sits in the box. A box's layout lists them in reading
order (getLayout().graphemes). Positions are in pixels in the box's own coordinates (0, 0 is its top-left
corner, before any rotation or scale), as the text is laid out flat.
Besides letters, the list holds a mark for each empty line (grapheme is "") and one for each paragraph's
end (grapheme is "\n"), so a caret index can point at every place the caret can stop.
baselineY: number
The baseline of the letter's line, in pixels. Every letter on a line shares it.
blockId?: string
The id of the paragraph (block) the letter is in.
color: string
The letter's color, as the document gives it.
fontFamily: string
The font family the letter is drawn in.
fontSize: number
The font size the letter is drawn at, in pixels.
grapheme: string
The letter as drawn (one user-perceived character, which may be several code points, such as an emoji).
height: number
The letter's height, in pixels: its ascent plus its descent.
width: number
The letter's advance width, in pixels, letter spacing not included.
x: number
The letter's left edge, in pixels. Includes the line's alignment and any list indent.
y: number
The top of the letter's box, in pixels: its baseline minus its ascent.
TextRenderingMode
type Konva or canvastype TextRenderingMode = "native" | "vector"
How letters are drawn: "vector" (the default) draws them as shapes from the font file, so every browser and the
print match; "native" lets the browser draw them. Set with configureRichText before any text exists.
TextUpgradePolicy
type Konva or canvastype TextUpgradePolicy = "never" | "immediately"
When text older than the latest version moves to it — the host's choice (upgradeOlderText):
- "never" (default): it keeps its version, edited or not - saved text never moves;
- "immediately": as it loads (it shifts by design; only a save persists it). An upgrade only ever goes up. (An "onEdit" choice was removed before 1.0: a version change re-wraps the whole box, so fixing one letter could move every line - docs/guides/upgrading.md.)
NativeLayoutComparison
interface Plain canvasinterface NativeLayoutComparison
What RichText.compareWithNativeLayout found: the box's lines as vector mode sets them and as native mode
would, and the characters the library draws from its own fallback fonts because the box's fonts lack them.
fallbackCharacters: string[]
The characters drawn from the library's fallback fonts (and emoji set) instead of the device's. These can
look different between the modes even when linesChanged is false.
linesChanged: boolean
True when the two modes break the text into different lines.
native: string[]
The text of each line as native mode would set it, top to bottom.
vector: string[]
The text of each line as the box sets it now (vector mode), top to bottom.
Editing sessions and undo
Which box is being edited, the change and formatting events, and undo kept in your app's own history.
createRichTextContext
function Plain canvascreateRichTextContext(options?: undefined | { history?: HistoryMode; onHistoryStep?: (step: HistoryStep) => void }): RichTextContext<RichText>
A new, independent editing session for enableRichText({ context }): its own undo history, its own box being
edited, its own listeners. history: who keeps its undo steps - "library" (the default), "host" (with
onHistoryStep, for your app's own undo) or "none" (previews and print). Stages given the same context share one
history.
| Parameter | Type | Description |
|---|---|---|
options |
undefined | { history?: HistoryMode; onHistoryStep?: (step: HistoryStep) => void } (optional) |
Returns RichTextContext<RichText>
defaultRichTextContext
const Plain canvasconst defaultRichTextContext: RichTextContext<RichText>
The editing session every stage joins unless enableRichText is given its own context: its undo history,
which box is being edited, and the formatting and change listeners your toolbar uses.
RichTextContext
class Plain canvasclass RichTextContext<Box>
One editor session's shared state: its undo history, its text boxes, which box is being edited, and its formatting listeners. Create one with createRichTextContext.
Every manager belongs to one context. Managers in the same context share one undo history, for example an editor with several pages and one manager per page. Managers in different contexts never affect each other, for example an offscreen copy of a design, a preview, or a second editor on the page. A manager given no context joins the default one.
onDocumentChange(callback: (box: Box) => void): () => void
A box's text or formatting changed: every change people make (typing, formatting, a resize, Story Mode), and
every undo, redo and restore. Called after the change is applied, once per change - each keystroke too, so
debounce an autosave. Changes your own code makes outside the library's commands (replaceVariables,
setDocument on the box itself) are not reported: your code made them. Returns a function that unsubscribes.
| Parameter | Type | Description |
|---|---|---|
callback |
(box: Box) => void |
Returns () => void
onFormattingChange(callback: (formatting: null | FormattingState, box: null | Box) => void): () => void
Formatting reports for this session's boxes: called with the formatting at the caret / selection
(bold, size, color…) and the box, whenever it changes — to keep a toolbar in step. It is also called
once right away with the latest report (null formatting when there is none). Returns a function that
unsubscribes.
| Parameter | Type | Description |
|---|---|---|
callback |
(formatting: null | FormattingState, box: null | Box) => void |
Returns () => void
restore(step: HistoryStep, which: "before" | "after"): number
"host" history mode: undo ("before") or redo ("after") a step the app kept — every box it
covers goes back to that snapshot, exactly as the library's own undo would put it (document,
size, position, scale, curve, caret). Boxes no manager of this context has any more (deleted)
are skipped; returns how many were restored.
| Parameter | Type | Description |
|---|---|---|
step |
HistoryStep |
|
which |
"before" | "after" |
Returns number
isEditingText
function Plain canvasisEditingText(): boolean
Is a text box being edited anywhere on the page (any editor session)? E.g. to let the box have the keys: an app's own shortcuts (Delete, arrows, Ctrl+A) should stand down while this is true.
Returns boolean
stopEditingText
function Plain canvasstopEditingText(): boolean
Stop editing text wherever it is on the page (e.g. before the app's own undo). True if it was.
Returns boolean
HistoryMode
type Konva or canvastype HistoryMode = "library" | "host" | "none"
Who keeps a context's undo steps:
"library"(the default): the context keeps the steps, and the library's undo and redo apply them. Best when the text is all your app undoes."host": the context keeps no steps. Each one is passed to youronHistoryStepcallback to store in your app's own undo stack; to undo or redo it, callcontext.restore(step, "before")orcontext.restore(step, "after"). Use this when your app already has an undo stack for its other objects."none": nothing is recorded. For offscreen work, previews and server-side rendering.
HistoryStep
interface Konva or canvasinterface HistoryStep
One undo step, as a host receives it. Steps that share group are ONE step to the user (letters
typed within a second, in the same box): keep the first step's before and take the latest
after. A step can cover several boxes (bold on two selected boxes is one step).
boxes: BoxChange[]
Each box the step changed, with its state before and after.
group: number
The step's group number. Consecutive steps with the same number are one step to the user: merge them
by keeping the first step's before states and the latest step's after states.
kind: "typing" | "deletion" | "formatting" | "structural"
What kind of change it was: "typing", "deletion", "formatting", or "structural" (a new paragraph, a paste, a whole-document replacement, a resize, Story Mode closing).
BoxChange
interface Konva or canvasinterface BoxChange
One box's change in an undo step: its state before and after.
after: DocumentSnapshot
The box's state after the change: what redo restores.
before: DocumentSnapshot
The box's state before the change: what undo restores.
id: string
The id of the text box that changed.
DocumentSnapshot
interface Konva or canvasinterface DocumentSnapshot
One text box's state at a moment, as an undo step keeps it: its document, caret, selection, size, position, scale and curve. Restoring a snapshot puts the box back exactly as it was. Treat it as opaque data: keep it and hand it back, rather than building or changing one.
cursorIndex: number
Where the caret was, as a character index.
document: RichTextDocument
The box's document at that moment.
documentCurve?: DocumentCurve
The box's curve settings, when it has any.
height: number
The box's height, in the box's own units (before its scale).
scaleX: number
The box's horizontal scale.
scaleY: number
The box's vertical scale.
selectionEnd: null | number
Where the selection ended, as a character index, or null when nothing was selected.
selectionStart: null | number
Where the selection started, as a character index, or null when nothing was selected.
width: number
The box's width, in the box's own units (before its scale).
x: number
The box's x position.
y: number
The box's y position.
Export and upgrade
Images and print from a Konva stage, and turning Konva.Text into rich text.
cloneStageForExport
function Plain canvascloneStageForExport(stage: Stage): Promise<Stage>
Clone a Konva stage and prepare all RichText nodes for export. This ensures cloned RichText nodes render properly by recalculating their layout and waiting for the rendering to complete.
When a stage is cloned, RichText nodes lose their graphemeMetrics data and have incorrect lifecycle states, preventing them from rendering. This helper function automatically fixes all RichText nodes in the cloned stage and waits for rendering to complete before returning.
Important: The cloned stage is removed from the global Konva.stages array to prevent interference with application state management and event handling. When you're done with the cloned stage, call cloned.destroy() to clean up resources.
| Parameter | Type | Description |
|---|---|---|
stage |
Stage |
The Konva stage to clone |
Returns Promise<Stage>
A promise that resolves to the cloned stage with all RichText nodes ready for export
import { cloneStageForExport } from '@mycreativeshop/rich-text/konva';
// Clone and prepare stage for export
const cloned = await cloneStageForExport(Konva.stages[0]);
// Now you can safely generate images
const dataURL = cloned.toDataURL();
const blob = await cloned.toBlob();
// Use the dataURL
const img = new Image();
img.src = dataURL;
document.body.appendChild(img);
// Clean up when done (recommended)
cloned.destroy();
convertKonvaTextToRichText
function Plain canvasconvertKonvaTextToRichText(konvaText: Text): Promise<RichText>
Converts a Konva.Text node to a RichText node with pixel-perfect positioning.
This function handles the conversion from Konva.Text's rendering to RichText's rendering, adjusting the Y coordinate to account for the difference in baseline calculations:
- Konva.Text uses: currentY + (ascent - descent) / 2 + lineHeight / 2
- RichText uses: currentY + ascent
The Y adjustment ensures the text appears at exactly the same position visually.
Attribute Handling:
- Font-related attributes (fontSize, fontFamily, fill, align, etc.) are transformed into the RichText document structure
- Non-font attributes (draggable, uiType, constraints, designStep, assetId, zIndex, etc.) are preserved as-is
- Transform attributes (x, y, width, scaleX, scaleY, rotation, offset, skew) are copied to maintain positioning
- Visual attributes (opacity, visible) and identity (id, name) are preserved
| Parameter | Type | Description |
|---|---|---|
konvaText |
Text |
The Konva.Text node to convert |
Returns Promise<RichText>
A RichText instance with all non-font attributes preserved
const konvaText = new Konva.Text({
x: 100,
y: 100,
text: 'Hello World',
fontSize: 24,
fontFamily: 'Arial',
draggable: true,
// Custom application attributes
assetId: 'abc-123',
constraints: { lockContent: true },
designStep: { order: 1, title: 'Step 1' }
});
// Convert to RichText (preserves custom attributes)
const richText = await convertKonvaTextToRichText(konvaText);
layer.add(richText);
// Custom attributes are preserved
console.log(richText.getAttr('assetId')); // 'abc-123'
console.log(richText.getAttr('constraints')); // { lockContent: true }
konvaText.destroy(); // Remove old node