Data

JSON documents

Try it in the demo →

Every text box holds a document: plain JSON that describes its paragraphs, its runs of styled text and its box-wide effects. Save it with your design, send it to a server, and load it in another app or on the other surface (Konva or a plain canvas). A document draws the same everywhere.

const json = JSON.stringify(box.getDocument());
// later
box.setDocument(JSON.parse(json));

The shape

{
  "baseFontSize": 18,
  "renderVersion": 2,
  "blocks": [
    {
      "id": "block-0",
      "type": "paragraph",
      "blockAttributes": {
        "fontSize": 48, "fontFamily": "Poppins", "color": "#5e2dc1",
        "alignment": "center", "lineHeight": 1.2, "letterSpacing": 0, "paragraphSpacing": 0.4
      },
      "content": [
        { "text": "Grand ", "attributes": {} },
        { "text": "opening", "attributes": { "bold": true, "color": "#ff7903" } }
      ]
    }
  ],
  "shadow": { "color": "rgba(0, 0, 0, 0.4)", "offsetX": 3, "offsetY": 4, "blur": 8 }
}

Paragraphs: blocks

Each block is a paragraph ("type": "paragraph") or a list item ("type": "list-item"), with an id unique in the document. Its look is in blockAttributes:

Field Is
fontSize, fontFamily, color The size in pixels, family and CSS color the paragraph's letters take unless a run sets its own
alignment "left", "center", "right" or "justify"
lineHeight A multiple of the font size; 1.2 is normal
letterSpacing Extra space after every letter, in pixels
paragraphSpacing Space after the paragraph, as a multiple of its font size
fontWeight Optional, 100-900
allCaps Optional: shows the paragraph in capitals while the words stay as typed
backgroundColor, borderColor, borderWidth Optional: a color behind the lines, and a border
tabStops Optional: see Tab stops
listType, listStyle, listIndent, bulletOffset, marker For list items: see Lists

Runs: content

A paragraph's text is a list of runs, each in one style. A run's attributes override its paragraph's look for those letters: fontSize, fontFamily, color, bold, italic, underline, strikethrough, fontWeight, and an outline (strokeColor, strokeWidth). A paragraph always has at least one run; an empty paragraph has one run with "text": "".

A soft line break (Shift+Enter) inside a paragraph is recorded on a run: startsWithSoftBreak: true when the run starts on a new line, and trailingSoftBreakCount for the breaks after its text.

Note: Letter spacing belongs to the paragraph. A run has no letter spacing of its own.

The whole box

Field Is
fill A gradient or an image painted through every letter instead of its color - see Gradient and image fills
shadow A drop shadow behind the letters - see Shadows and outlines
fontMetadata Where the document's custom fonts live, recorded as they are applied, so the design opens without your font lookup - see Fonts
baseFontSize The size, in pixels, of new text with no size of its own, such as an empty box's cursor
renderVersion The text version the document draws with - see Vector text. Leave it as the library writes it.

A curve is not part of the document. It is the box's documentCurve setting - see Curved text.

The full types are RichTextDocument, Block, BlockAttributes, InlineContent and InlineAttributes.

Read and replace a box's document

Call Does Undo step
box.getDocument() Returns the box's document
box.setDocument(doc) Replaces it and lays the box out. Fonts the box does not have yet are fetched, and the box lays itself out again when they arrive. For loading a design. No
rt.manager.setDocument(doc, [box]) (plain canvas: rt.setDocument(doc, [box])) Replaces it as an edit Yes

getDocument() returns the live object the box draws, not a copy. Don't change it in place: copy it, change the copy, and pass it to setDocument.

Change only the words

getText reads a box's plain text, and setText gives it new words with the same look:

// Konva
rt.manager.getText([card]); // "Maria Lopez\nOwner", paragraphs joined by newlines
rt.manager.setText("Ana Ruiz\nManager", [card]);

// Plain canvas
rt.getText([card]);
rt.setText("Ana Ruiz\nManager", [card]);

setText makes one paragraph per line. Every paragraph takes the look of the box's first letter and its paragraph, so a bulleted line stays bulleted. The box's fill, shadow and font addresses are kept. It is one undo step. With no boxes named, both act on the box being edited, else the selected box.

Make a document

import { createDocumentFromText } from "@mycreativeshop/rich-text"; // on Konva: "@mycreativeshop/rich-text/konva"

createDocumentFromText("Grand opening\nThis Saturday", 48, "Poppins", "#5e2dc1");

createDocumentFromText(text, fontSize?, fontFamily?, color?) makes one paragraph per line, all in one style. Blank lines between paragraphs are kept as empty paragraphs, and blank lines at the end are dropped. The defaults are 16 pixels, Poppins and black.

Save and load a design

Konva. A RichText stores its document in its attributes, so Konva's own toJSON() saves it, and Konva.Node.create(json) restores the boxes as RichText nodes. To save only the text, save box.getDocument().

Plain canvas. Save what you passed to addBox, read back from the box:

const saved = rt.boxes().map((box) => ({
  x: box.x(),
  y: box.y(),
  width: box.width(),
  rotation: box.rotation(),
  document: box.getDocument(),
  documentCurve: box.attrs.documentCurve,
  editable: box.attrs.editable !== false,
}));
// later
for (const config of saved) rt.addBox(config);

A box's height is not saved: it follows the text.

Save as people work

onDocumentChange is called after every change people make - typing, formatting, a resize, Story Mode - and after every undo and redo. It is called for each keystroke, so wait for a pause before saving:

let timer: ReturnType<typeof setTimeout>;
const rt = enableRichText(stage, {
  onDocumentChange: (box) => {
    markUnsaved();
    clearTimeout(timer);
    timer = setTimeout(save, 1000);
  },
});
// Plain canvas: createCanvasRichText({ onChange: render, onDocumentChange: (box) => ... })

Changes your own code makes with box methods such as box.setDocument are not reported. See Your shortcuts and editing events.

Bring back an emptied box

box.getLastNonEmptyDocument() returns the last document the box had with text in it, or null if it never had any. Use it to restore a box someone emptied, for example after your app removed it in onEmptyBlur.

In the API reference