Editing

Story Mode

Try it in the demo →

Story Mode opens a text box's text in an HTML editor panel over the canvas, where long text is easier to write than on the canvas itself, especially on phones. The panel shows the text with its styles, uses the browser's own text editing and keyboard, and writes each edit to the box as it is typed. Done closes the panel; Cancel puts the text back the way it was. Story Mode works the same on Konva and on a plain canvas.

Open it

Story Mode opens on the box being edited, or else on the selected box:

// Konva
storyButton.onclick = () => {
  if (rt.manager.selected()) rt.manager.enterStoryMode();
};

// Plain canvas
storyButton.onclick = () => {
  if (rt.selected()) rt.enterStoryMode();
};

To open it on a particular box, such as the one a list in your app points at, pass the box: rt.manager.enterStoryMode(box) (plain canvas: rt.enterStoryMode(box)).

With no box given and none selected, enterStoryMode() throws an error, so check first. If the box was being edited, editing stops and the panel opens with the cursor at the end of the text. isStoryModeActive() says whether the panel is open.

Done and Cancel

The panel has Done and Cancel buttons:

  • Done closes the panel. The box already has the edited text, because each edit reached it as it was typed.
  • Cancel puts the box's text back the way it was when the panel opened, then closes the panel. Esc does the same.

On a narrow screen, a tap outside the editor closes the panel like Done. Your code can close it the same two ways:

// Konva
rt.manager.exitStoryMode();   // Done: returns the box, or null
rt.manager.cancelStoryMode(); // Cancel

// Plain canvas
rt.exitStoryMode();
rt.cancelStoryMode();

Each edit in the panel is recorded in the undo history the way typing on the canvas is, and Cancel is one more step that puts the text back. onDocumentChange hears every one of these changes.

If Done leaves the box with no text, onStoryModeExit is called as for any Done, and then the box fires the same event as leaving edit mode with no text: onEmptyBlur, so your app can remove the empty box the way it already does. exitStoryMode() returns null in that case.

A box marked editable: false doesn't open in Story Mode, as it doesn't start editing on a click.

Format text while the panel is open

Your toolbar keeps working while the panel is open. A formatting command changes the words selected in the panel, or the cursor's paragraph when the panel shows only a cursor, and the panel shows the change. Undo and redo from your own buttons show in the panel too.

Inside the panel, the browser's own Ctrl+B, Ctrl+I and Ctrl+U are blocked, because formatting comes from your app. Shortcuts your app binds on the window, such as the ones in Keyboard and mouse, reach the words selected in the panel. A paste into the panel arrives as plain text.

Hear when it opens and closes

// Konva
const rt = enableRichText(stage, {
  onStoryModeEnter: (box) => hideCanvasTools(),
  onStoryModeExit: (box, wasSaved) => showCanvasTools(), // wasSaved: true for Done, false for Cancel
});

// Plain canvas
const rt = createCanvasRichText({
  onChange: render,
  onStoryModeEnter: (box) => hideCanvasTools(),
  onStoryModeExit: (box, wasSaved) => showCanvasTools(),
});

Where the panel opens

On a screen wider than 768 pixels, the canvas is dimmed and a 400 by 300 pixel panel opens just above or below the box, on the side with more room, lined up with the box's left edge and kept inside the canvas. On a screen 768 pixels wide or narrower, the panel opens in the middle of the dimmed canvas. Without a position for the box (a plain canvas with no storyModePlacement), the panel opens in the middle of the page.

  • On Konva the library finds the box on the stage itself, with the stage's zoom and pan and any groups included.
  • On a plain canvas the library does not know where your canvas is on the page or how you have zoomed it, so the panel opens in the middle of the page. To open it next to the box, pass storyModePlacement: the element your canvas is drawn in, and the box's rectangle in that element's CSS pixels.
const rt = createCanvasRichText({
  onChange: render,
  // zoom, panX and panY: how your scene is drawn on the canvas, in CSS pixels
  storyModePlacement: (box) => ({
    container: canvas,
    box: { x: box.x() * zoom + panX, y: box.y() * zoom + panY, width: box.width() * zoom, height: box.height() * zoom },
  }),
});

storyModePlacement works on Konva too: pass it to enableRichText, and it replaces the position the stage gives.

Show it in your own panel

To show the editor inside your own side panel or dialog instead of over the canvas, give Story Mode the element before you open Story Mode. Story Mode adds no Done or Cancel buttons to your own panel, so add your own; Esc in the editor still cancels.

const commands = rt.manager; // plain canvas: const commands = rt;

function openStory() {
  commands.setStoryModeContainer(panel.querySelector(".story-editor") as HTMLElement);
  commands.enterStoryMode();
  panel.hidden = false;
}

doneButton.onclick = () => {
  commands.exitStoryMode();
  panel.hidden = true;
};
cancelButton.onclick = () => {
  commands.cancelStoryMode();
  panel.hidden = true;
};

setStoryModeContainer(undefined) goes back to the panel over the canvas. The editor fills your element and scrolls, and sets no font, size or color of its own: each run of text carries its own. Style its padding and background with the class story-mode-editor.

Text in your panel is shown at the size it looks on the canvas. On Konva the library works that size out from the stage's zoom and the box's groups. On a plain canvas, pass your zoom:

const rt = createCanvasRichText({ onChange: render, storyModeScale: () => zoom });

In either place, text too small to read is shown larger, and very large text is shown smaller so that it fits the panel's width.

Options

Option Surface What it does
onStoryModeEnter(box) both Called when the panel opens
onStoryModeExit(box, wasSaved) both Called when the panel closes: wasSaved is true for Done, false for Cancel
storyModePlacement(box) both Where the box is on screen; required on a plain canvas to open the panel next to the box
storyModeScale(box) both How large the box's text looks on screen (your zoom), for the text size in the panel

Limits

Limit: The panel is a fixed 400 by 300 pixels on a wide screen. To give long text more room, show the editor in your own panel with setStoryModeContainer.

In the API reference