An editor app has its own keyboard shortcuts: Delete removes the selected object, the arrow keys move it. While someone types in a text box, those keys belong to the text, so Delete removes a letter instead of the box. This page shows how your shortcuts step aside during editing, how your app hears when editing starts and stops, and how it removes empty boxes and saves as people work. It ends with every event the library offers.
Let the keys go to the text while someone types
Check whether text is being edited at the top of your key handler. On Konva, isEditingText() answers for the whole
page: any stage, any context. On a plain canvas, rt.editing() returns the box being edited, or null.
// Konva
import { isEditingText } from "@mycreativeshop/rich-text/konva";
window.addEventListener("keydown", (e) => {
if (isEditingText()) return; // the keys are the text's
if (e.key === "Delete" || e.key === "Backspace") removeSelected();
if (e.key.startsWith("Arrow")) nudgeSelected(e.key, e.shiftKey ? 16 : 4);
});
// Plain canvas: if (rt.editing()) return;
While a box is being edited, the library listens for keys on the window. It uses the typing keys, Backspace, Delete,
Enter, Tab, the arrow keys, Home and End, Esc, and Ctrl (Cmd on a Mac) with A, C, X, V, Z and Y. While the keyboard
focus is in an input, a textarea, a select or an element with contenteditable, the library leaves every key alone.
Note: Esc ends editing and leaves the box selected. The library calls
preventDefault()on the Esc press, so if your app also uses Esc (to deselect, for example), checke.defaultPreventedto let that press only end editing.
Bind Bold, Italic and Underline
The library does not bind Ctrl+B, Ctrl+I or Ctrl+U (Cmd on a Mac). While a box is being edited, a letter key pressed
with Ctrl or Cmd is not typed, and your app's handler receives it. Bind the keys to the commands, and call
preventDefault() so the browser's own action for the key does not run:
window.addEventListener("keydown", (e) => {
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
if (/^(INPUT|TEXTAREA|SELECT)$/.test((e.target as HTMLElement).tagName)) return; // a form field keeps its keys
const command = { b: "toggleBold", i: "toggleItalic", u: "toggleUnderline" }[e.key.toLowerCase()];
if (!command || !(rt.manager.editing() || rt.manager.selected())) return; // plain canvas: rt.editing() || rt.selected()
e.preventDefault();
rt.manager[command](); // plain canvas: rt[command]()
});
With a box selected and not being edited, the command formats all of its text, as a toolbar button does.
End editing from your app
stopEditingText() (Konva) ends editing wherever it is on the page and returns true if a box was being edited.
rt.manager.stopEditing() ends it on that stage, and rt.stopEditing() on a plain canvas. The box stays selected.
Use it when people pick another tool or open a dialog.
import { stopEditingText } from "@mycreativeshop/rich-text/konva";
shapeTool.onclick = () => {
stopEditingText();
startShapeTool();
};
Hear when editing starts and stops
Pass onEnterEditMode and onExitEditMode to show a text toolbar while someone types, or to hide other tools.
// Konva
const rt = enableRichText(stage, {
onEnterEditMode: (box) => showTextToolbar(box),
onExitEditMode: () => hideTextToolbar(),
});
// Plain canvas
const rt = createCanvasRichText({
onChange: render,
onEnterEditMode: (box) => showTextToolbar(box),
onExitEditMode: () => hideTextToolbar(),
});
Editing has already ended everywhere when onExitEditMode is called: isEditingText() is false inside it.
Remove boxes left empty
onEmptyBlur is called when editing ends on a box with no words in it (only spaces or line breaks), just before
onExitEditMode. It is also called when Story Mode closes and leaves a box empty. Remove the box there:
// Konva: a destroyed box is released by the library
const rt = enableRichText(stage, { onEmptyBlur: (box) => box.destroy() });
// Plain canvas
const rt = createCanvasRichText({ onChange: render, onEmptyBlur: (box) => rt.removeBox(box) });
box.getLastNonEmptyDocument() returns the box's last document that had words in it, or null. Keep it in your own
undo history if people should be able to bring the box back.
Save as people work
onDocumentChange is called after every change people make to a box's text or formatting: typing, a formatting
command, a resize, a Story Mode edit. It is also called after every undo and redo, and after every
restore in host history. Use it to save as people work, or to show that there are unsaved changes.
It is called once per change, which means once 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) => ... })
- Commands your own code calls, such as
rt.manager.setText(...), are changes too and are reported. - Changes your code makes directly on a box, such as
box.setDocument(...)orreplaceVariables, are not reported: your code already knows about them. - On Konva, each stage's
onDocumentChangehears only its own boxes. To hear every box of an editing session with several stages, listen on the context:rt.manager.context.onDocumentChange((box) => ...). It returns a function that stops listening.
Take over the undo keys
In the default history, the library undoes on Ctrl+Z and redoes on Ctrl+Shift+Z and Ctrl+Y (Cmd on a Mac) while a
box is being edited. On Konva, pass disableInternalUndoRedoShortcuts: true to enableRichText to bind those keys
yourself; your handler then calls rt.manager.undo() and rt.manager.redo().
You don't need it in "host" history mode, where the library does nothing with the undo keys and your app's own undo handles them. A plain canvas has no such option: use host history to own the undo keys there. See Your app's undo.
Every event
Pass these callbacks to enableRichText(stage, { ... }) on Konva, or to createCanvasRichText({ ... }) on a plain
canvas, unless the table says otherwise. Most apps use the first table; the second is for particular needs.
What most apps use
| Callback | When it is called | Typical use |
|---|---|---|
onFormattingChange |
The selection's formatting changes | Keep your toolbar in step. On the context on Konva (defaultRichTextContext.onFormattingChange(...)), on rt on a plain canvas. See Your own toolbar |
onDocumentChange |
After every change people make, and every undo, redo and restore | Save as people work (wait for a pause), or show an "unsaved changes" mark |
onEnterEditMode(box), onExitEditMode(box) |
Editing starts and stops | Show a text toolbar while someone types |
onEmptyBlur(box) |
Editing ends on a box with no words in it | Remove the empty box |
onChange (plain canvas only) |
The canvas needs drawing again | Required on a plain canvas: schedule your render |
For particular needs
| Callback | When it is called | Typical use |
|---|---|---|
onHistoryStep(step) |
Each undo step, in "host" history mode. On Konva, pass it to createRichTextContext |
One undo history for your whole app. See Your app's undo |
onStoryModeEnter(box), onStoryModeExit(box, wasSaved) |
Story Mode opens and closes | Hide your own UI while it is open. See Story Mode |
fonts.onFontError (in configureRichText) |
A font can't load | Report it. See Fonts |
onSelectionChange(boxes) (Konva) |
The selected text boxes change | UI that depends on the selection |
onClipboardAccessDenied (Konva) |
The browser refused to let the page read the clipboard | Suggest Ctrl+V |
onBeforeMouseDown(e, box) (Konva) |
Before a press on a box reaches the library | Return true to let another tool take the press |
Konva node events
On Konva each box also fires events you can listen to with box.on(...): richtext:enterEditMode,
richtext:exitEditMode, richtext:emptyBlur, richtext:transformStart and richtext:transformEnd (a resize with
the transformer starts and ends).