Lynx/Modules/Markdown/MarkdownEditor
@sigx/lynx-markdown · Stable · Component library

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#

TSX
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.

TSX
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#

KeyBehaviour
EnterSplit the block (a heading continues as a paragraph; in a list, a new item)
Backspace at a block's start / Delete at its endJoin with the neighbouring block
ArrowUp / ArrowDown on the first / last lineMove to the neighbouring block
Tab / Shift-TabIndent / outdent a list item
EscapeSelect 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.

TSX
<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.

TSX
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:

TSX
<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:

MethodDescription
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.
editorThe 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.

  • SafeAreaProvider for the keyboard hooks. The popup and the fullscreen overlay read keyboard height via @sigx/lynx-keyboard's useKeyboard(), which needs a <SafeAreaProvider> ancestor. Without one it reads height 0 (no crash, no warning).
  • ignore-focus keeps the keyboard up. The toolbar, popup and fullscreen close affordance carry ignore-focus so 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 the minLines / maxLines window scales with it, live.

Props#

PropTypeDescription
valuestringMarkdown source. An external change replaces the document; echoes of onChange are ignored.
documentRootAn mdast document instead of value (wins for the initial content).
onChange(markdown: string) => voidMarkdown after every committed change (never mid-composition).
onDocumentChange(doc: Root, transaction: Transaction) => voidThe tree after every committed change.
onSelectionChange(selection: EditorSelection) => voidKey-addressed selection changes.
placeholderstringPlaceholder shown while empty.
minLines / maxLinesnumberHeight window in lines; content past maxLines scrolls.
mode'auto' | 'fixed' | 'fullscreen'Layout mode.
fontSize / textColor / accentColor / placeholderColornumber / stringField typography and colours.
confirmType'send' | 'search' | 'next' | 'go' | 'done'Return-key label hint.
autoFocus / disabledbooleanFocus on mount; read-only.
toolbarboolean | 'top' | 'bottom'Mount the built-in toolbar.
toolbarItems / renderToolbarItemreadonly ToolbarItem[] / ToolbarRenderItemBase items; per-item renderer.
pluginsreadonly RichTextPlugin[]Plugins with an editor slice. Captured at mount.
componentsPartial<LynxMarkdownComponents>Components for void blocks (dividers, definitions).
keymapKeymapExtra bindings over the core's.
inputRulesreadonly InputRule[] | falsefalse disables input rules; an array adds to them.
suggestionPopupSuggestionPopupStyleStyle the built-in popup; omitted colours auto-tint from textColor.
suggestions'popup' | 'none''none' leaves rendering to renderSuggestions / onTriggerSession.
renderSuggestions(api: SuggestionsApi) => JSXElement | nullRender the suggestion list yourself.
onTriggerSession(session: TriggerSession | null) => voidObserve trigger sessions.
fullscreenClass / onFullscreenChangestring / (open: boolean) => voidFullscreen overlay styling and notification.
onFocus / onBlur() => voidFocus changes.
classstringRoot class.
controllerRef(c: MarkdownEditorController) => voidReceives the imperative controller.

See the API reference for the types and the editor-subpath exports.