MarkdownEditor
A block-tree markdown editor on native fields — the Lynx host of the @sigx/richtext editor core. Markdown in via value, onChange(markdown) out; or the mdast tree via document.
Import
import { MarkdownEditor } from '@sigx/lynx-markdown/editor';
MarkdownEditor lives on the @sigx/lynx-markdown/editor subpath. Importing the subpath turns the optional peers @sigx/lynx-richtext (the rich-text element — native on iOS/Android, contenteditable on web) and @sigx/lynx-keyboard into real, required dependencies — install both alongside the package and run sigx prebuild.
How it works
One createEditor() per component, with the mdast Root as the document. Every root block renders as a keyed BlockView: paragraphs and headings as native <sigx-richtext> fields in boundary-keys mode, code blocks as a <textarea>, lists, quotes and tables as views. State, history, commands, keymap, input rules, plugins and trigger sessions are the core's and behave as on the web; untouched blocks are never re-rendered.
The editor reads and writes markdown through markdownFormat and installs markdownPreset itself, so markdown shortcuts work out of the box: # for a heading, - / 1. / - [ ] for lists, > for a quote, **bold**, *italic*, `code`, ~~strike~~, [text](url), and a code fence or --- on Enter.
Basic usage
An incoming value equal to the last emitted markdown is treated as the editor's own echo and ignored, so you can feed onChange output straight back in. Any other change to value replaces the document.
import { MarkdownEditor } from '@sigx/lynx-markdown/editor';
function Composer() {
return (
<MarkdownEditor
value={draft.value}
placeholder="Write something"
onChange={(markdown) => { draft.value = markdown; }}
/>
);
}
To store the tree rather than markdown, bind document / onDocumentChange(doc, transaction) instead — or both.
Keyboard
| Key | Behaviour |
|---|---|
| Enter | Split the block (a heading continues as a paragraph; in a list, a new item) |
| Backspace at a block's start / Delete at its end | Join with the neighbouring block |
| ArrowUp / ArrowDown on the first / last line | Move to the neighbouring block |
| Tab / Shift-Tab | Indent / outdent a list item |
| Escape | Select the block |
These keys reach the core through the field's bindboundarykey event. On a native build without it, a typed newline still splits the block, but Backspace / Delete at a block's edges stay in-block and arrows do not cross blocks.
Toolbar
Pass toolbar (true or 'bottom' below the content, 'top' above) to mount the built-in EditorToolbar over the core's defaultToolbarItems plus plugin items. toolbarItems replaces the base items and renderToolbarItem skins each button; @sigx/lynx-daisyui/markdown ships daisyToolbarItem for that.
<MarkdownEditor value={draft.value} onChange={save} toolbar="bottom" />
Plugins
plugins takes the same RichTextPlugin[] MarkdownView does. A plugin's editor slice adds triggers, toolbar items, commands, keymap bindings and input rules. Two ship ready-made: createMentionPlugin (@ with candidate search) and the core's createSlashPlugin (/ block menu), both importable from here. A baseline mention plugin is always registered so insertChip round-trips without one. Plugins are captured at mount.
import { MarkdownEditor, createSlashPlugin } from '@sigx/lynx-markdown/editor';
import { createMentionPlugin } from '@sigx/lynx-markdown';
const plugins = [
createMentionPlugin({ search: async (q) => (await searchDirectory(q)).map((p) => ({ id: p.id, label: p.name })) }),
createSlashPlugin(),
];
function Composer() {
return <MarkdownEditor value={draft.value} onChange={save} plugins={plugins} toolbar />;
}
Suggestions
By default a SuggestionPopup opens inside the block the trigger session belongs to. Style it with the optional suggestionPopup prop — surface, border, active and text colours plus width, maxHeight, placement and class. Every field is optional: any colour you omit auto-tints from the editor's textColor, so the popup matches a dark or light field without configuration. (daisyUI wires concrete theme colours through useMarkdownEditorTheme().suggestionPopup from @sigx/lynx-daisyui/markdown.) Per-row content is the plugin's renderItem.
To draw the list yourself — docked above the keyboard in a chat composer, say — pass suggestions="none" and renderSuggestions:
<MarkdownEditor
plugins={plugins}
suggestions="none"
renderSuggestions={({ session, activeIndex, pick }) => (
<view class="suggest-row">
{session.items.map((item, i) => (
<view key={item.id} class={i === activeIndex ? 'pill active' : 'pill'} bindtap={() => pick(item)}>
<text>{item.label}</text>
</view>
))}
</view>
)}
/>
onTriggerSession(session | null) reports sessions as they open, update and close, for a host that renders the list elsewhere.
Imperative control
controllerRef yields a MarkdownEditorController:
| Method | Description |
|---|---|
toggleBold() / toggleItalic() / toggleStrike() / toggleCode() | Toggle inline marks on the selection. |
setHeading(level) | 1–6 sets a heading; 0 reverts to paragraph. |
setList(kind) | 'bullet' | 'ordered' | 'task' | 'none'. |
toggleQuote() | Toggle a blockquote. |
insertLink(href, text?) / insertText(text) / replaceRange(start, end, text) | Insert or replace content in the current block. |
insertChip(chip, replace?) | Insert a mention chip at the caret, optionally replacing [from, to). |
getMarkdown() / setMarkdown(md) | Read / replace as markdown. |
getDocument() / setDocument(doc) | Read / replace the mdast tree. |
getSelection() | The key-addressed EditorSelection. |
undo() / redo() | History. |
run(command | name) | Run any core command by function or registered name. |
editor | The core Editor instance. |
focus() / blur() / clear() | Focus and reset. |
openFullscreen() / closeFullscreen() / isFullscreen() | Fullscreen control — restyles the same element in place, never re-parented. |
Native setup & gotchas
These apply only to the editor subpath — the renderer needs none of them.
SafeAreaProviderfor the keyboard hooks. The popup and the fullscreen overlay read keyboard height via@sigx/lynx-keyboard'suseKeyboard(), which needs a<SafeAreaProvider>ancestor. Without one it reads height0(no crash, no warning).ignore-focuskeeps the keyboard up. The toolbar, popup and fullscreen close affordance carryignore-focusso taps never blur the editor.- Fullscreen restyles in place. Going fullscreen re-styles the same mounted element (a fixed inset overlay); it is never re-parented, which would recreate the native views and lose the document.
- Text size follows the OS.
fontSize(default 16) is multiplied by the effective font scale, and theminLines/maxLineswindow scales with it, live.
Props
| Prop | Type | Description |
|---|---|---|
value | string | Markdown source. An external change replaces the document; echoes of onChange are ignored. |
document | Root | An mdast document instead of value (wins for the initial content). |
onChange | (markdown: string) => void | Markdown after every committed change (never mid-composition). |
onDocumentChange | (doc: Root, transaction: Transaction) => void | The tree after every committed change. |
onSelectionChange | (selection: EditorSelection) => void | Key-addressed selection changes. |
placeholder | string | Placeholder shown while empty. |
minLines / maxLines | number | Height window in lines; content past maxLines scrolls. |
mode | 'auto' | 'fixed' | 'fullscreen' | Layout mode. |
fontSize / textColor / accentColor / placeholderColor | number / string | Field typography and colours. |
confirmType | 'send' | 'search' | 'next' | 'go' | 'done' | Return-key label hint. |
autoFocus / disabled | boolean | Focus on mount; read-only. |
toolbar | boolean | 'top' | 'bottom' | Mount the built-in toolbar. |
toolbarItems / renderToolbarItem | readonly ToolbarItem[] / ToolbarRenderItem | Base items; per-item renderer. |
plugins | readonly RichTextPlugin[] | Plugins with an editor slice. Captured at mount. |
components | Partial<LynxMarkdownComponents> | Components for void blocks (dividers, definitions). |
keymap | Keymap | Extra bindings over the core's. |
inputRules | readonly InputRule[] | false | false disables input rules; an array adds to them. |
suggestionPopup | SuggestionPopupStyle | Style the built-in popup; omitted colours auto-tint from textColor. |
suggestions | 'popup' | 'none' | 'none' leaves rendering to renderSuggestions / onTriggerSession. |
renderSuggestions | (api: SuggestionsApi) => JSXElement | null | Render the suggestion list yourself. |
onTriggerSession | (session: TriggerSession | null) => void | Observe trigger sessions. |
fullscreenClass / onFullscreenChange | string / (open: boolean) => void | Fullscreen overlay styling and notification. |
onFocus / onBlur | () => void | Focus changes. |
class | string | Root class. |
controllerRef | (c: MarkdownEditorController) => void | Receives the imperative controller. |
See the API reference for the types and the editor-subpath exports.
