Customizing the Default Editor
This page gathers every modification available without abandoning the default implementation, ordered from least to most invasive: the style configuration, the theme, buttons added to the sidebar and to nodes, replacing an individual renderer, and finally where to take over if you drop the layer entirely.
Style configuration
The config prop takes a partial configuration: write only what you want changed, the rest
keeps its default. The editor's configuration has three parts:
| Field | Contents |
|---|---|
margins | background (between the sheet and its contents), paragraph (between paragraphs), small (minor spacing) |
widths | editable_drawer (width of pop-out drawers), minimum_content (minimum width of an element with content) |
fonts | body (running text), structure (structural text such as node labels), info (hints) |
Each font is a set of fontFamily, fontSize, lineHeight and the
like. body is what the author types, structure is text the interface itself
produces (node labels, words on buttons), and info is auxiliary hints.
<DefaultEditorComponent
editorcore={editor_core}
config={{
margins: {paragraph: "1rem"},
fonts : {body: {fontFamily: "Georgia", fontSize: "1.1rem"}},
}}
/>
This configuration is separate from the printer's, because the editing interface and the printed output ought to look different: while editing you may want generous line spacing to make clicking easier, whereas the output should be compact and well set. For the printer side see The Printer Side.
Interface text
Every string the default implementation puts on screen (button hints, panel titles, the messages raised
after an operation) lives in one text table. It defaults to Chinese and is overridden partially through
the texts prop, exactly like config:
<DefaultEditorComponent
editorcore={editor_core}
texts={{
buttons: {delete_node: "Delete node", unwrap_node: "Unwrap node"},
areas : {concept_title: "Insert Concept", parameter_title: "Edit Parameters"},
}}
/>
The full set of fields is the EditorTexts type, in three parts: buttons for
button hints, areas for panel titles and concept type labels, and messages for
toasts. The library ships no i18n framework of its own, so manage translations however you like and hand
the finished strings over. The live demo does exactly this: Chinese uses the library defaults, English
passes an override, in demo/src/editor_texts.ts.
Theme
The default implementation uses MUI's theme throughout: colours, corner radii and button styles all come
from the theme, and none are hard-coded. Changing the palette therefore means wrapping a
ThemeProvider around it, dark mode included, and both editor and printer follow:
<ThemeProvider theme={createTheme({palette: {mode: "dark"}})}>
<DefaultEditorComponent editorcore={editor_core} />
</ThemeProvider>
The split between theme and config is that the theme governs colour and control appearance while the config governs fonts, margins and widths. To change the background colour of a theorem block, look at the theme; to change how much space sits between paragraphs, look at the config.
Sidebar buttons and a trailing element
sidebar_extras takes an array of components appended after the four built-in sidebar buttons,
with a divider inserted automatically. Appended buttons join the keyboard navigation on their own (hold
Alt+E, then move with the up and down arrows); nothing needs registering. Inside a button,
useEditor() gives you the editor:
import { useEditor, AutoIconButton } from "@project-callio/calliotext"
import { Download as DownloadIcon } from "lucide-react"
function ExportButton(){
const editor = useEditor()
return <AutoIconButton
icon={DownloadIcon} title="Export" size="medium"
onClick={() => {
const root = editor.get_root()
console.log(JSON.stringify(root))
}}
/>
}
<DefaultEditorComponent editorcore={editor_core} sidebar_extras={[ExportButton]} />
AutoIconButton is the icon button the default implementation uses for itself; it hooks into
the key hints and the theme automatically, so a button built with it looks like the built-in ones. You
can of course return a plain MUI IconButton instead, at the cost of handling hints and
styling yourself.
end_element is arbitrary content placed below the sheet, suitable for a status bar, a word
count and similar.
Buttons on a node
Sidebar buttons act on the whole document. For an operation that acts on one node, the place to add it is
that node's own button group, through the buttons_extra option of the renderer factory. Such
a button can reach its node with useNode() and useParameters():
function MarkDoneButton(){
const editor = useEditor()
const node = useNode()
return <AutoIconButton
icon={CheckIcon} title="Mark done" size="medium"
onClick={() => {
editor.set_node(node, {parameters: {
...node.parameters,
done: {val: true, type: "boolean"},
}})
}}
/>
}
const theorem_editor = get_deafult_group_editor_with_appbar({
get_label : () => useParameters().category,
buttons_extra: [MarkDoneButton],
})
Replacing a single renderer
If one concept's appearance is too far from what the factories offer, replace just that renderer and
leave the rest alone. A renderer is an ordinary React component receiving three props,
editor, node and children:
import { NodeInfoProvider, EditorRendererProps } from "@project-callio/calliotext"
const bare_editor = ({node, children}: EditorRendererProps) => (
<NodeInfoProvider node={node}>
<div style={{borderLeft: "3px solid #888", paddingLeft: "1rem"}}>
{children}
</div>
</NodeInfoProvider>
)
Writing your own renderer comes with two hard requirements. First, children must reach the
screen, or the child content is lost and Slate raises an error for the DOM it cannot find. Second, if you
want to call hooks such as useParameters() inside, or to place buttons from the default
implementation, you have to wrap a NodeInfoProvider yourself, since that layer was
previously supplied by the factory.
One more thing, not required but worth doing: put the parts that should not be selectable (buttons,
labels) inside an EditorUnselecableBox. Without it, a Backspace at the start of the node may
delete these interface elements.
Dropping the default implementation
The deepest level is to skip this layer and build an interface on EditorComponent. What you
then have to handle yourself is exactly the list in the table on the
overview page: the style configuration context, the toast container, index
conflict repair, key dispatch, and handing the editor instance to downstream components. All of these are
publicly exported, and the source of DefaultEditorComponent is itself a template you can
copy from.