Rich text / foldkit-richtext-dom

Configuring a placement

editorAt(hostId, placement) takes, each optional:

  • rendering: how marks and node kinds become elements; the default renders the core's own names. A mark renders inside its run element, so the run stays outermost and a selection still maps.

  • vocabulary: { marks, nodes } registries. The editor refuses an edit a declaration forbids, rather than only letting validate report it later.

  • inputRules: rules applied to what is typed, so a marker turns into a block change as it is completed, such as foldkit-richtext-markdown's markdownInputRules. A rule the vocabulary refuses does not cost the keystroke: the text is inserted on its own.

  • decorate: document => DecorationSet, drawn over the document on the mount and every patch, code highlighting for one. It sees the document only. A highlight derived from application state, such as a search query or other people's carets, goes through the overlay(hostId, decorations) Command instead. It draws them after the placement's own and leaves the caret where it is. Its positions name the document shown, so set it again when that changes; placing the host id again starts it with none. focus(hostId, selection) is the Command that sends the reader into the text from a control outside it (a table of contents, a find bar closing on its match): the editor takes the focus with that selection, scrolled to the middle of the window when it is out of view. The browser reports the selection back as Selected.

  • slashEntries: what its slash menu offers, which Enter in a query resolves against: an application's own catalogue (an image, a table, a callout), beside or instead of the editor's slashEntries. The application's menu view draws the same list.

  • An empty line holds a <br data-richtext-filler> after its run. A browser can neither draw a caret in an empty text node nor move one there with the arrow keys, so without it a new line made with Enter had no height and could not be reached. The caret on an empty line stands before it; it holds no text or position, server markup draws it too, and an import drops it.

  • placeholder: what a blank document shows (RichText.isBlank). It is put on the block as data-placeholder, and the root, a role="textbox", carries it as aria-placeholder. Drawing it is the stylesheet's, so the text never enters the content:

    css
    [data-placeholder]::before {
      content: attr(data-placeholder);
      float: left;
      height: 0;
      pointer-events: none;
      color: GrayText;
    }
  • lineHint: what the empty paragraph a collapsed caret is in shows, such as "Type / for blocks", put on it as data-hint and moved as the caret moves. A blank document's block shows the placeholder instead. Draw it like the placeholder, under :focus so it shows only while the editor is being written in: [contenteditable]:focus [data-hint]::before.

  • serverRendered: whether a render is the server's; see "On a server-rendered page".

Each has a record at foldkit-richtext-dom/host (placeRendering/renderingFor, placeVocabulary/vocabularyFor, and so on) that editorAt writes. It is kept for the host id, not for one mount, so a host that unmounts and mounts again renders the same way. One host id is one editor on the page: the patch Command finds its host by id.

The assembled parent's update handles every editor Message. Build them with edited and the helpers beside it: typed, pressed, toggled, applied, cleared, retyped, wrapped, converted, lifted, moved, selected, undone, redone, and patched.

When the document changes from outside

A document the parent puts in the Model any other way than an edit (an entry opened, a revision restored, a form's fill or Reset) gets a fresh host. The host's Mount reads its document once, so the view keys the host by the document it shows: each committed edit hands its key on to the next document, and a document that did not come from an edit here has none yet. The first document a host id shows is drawn without a key, as the server draws it.

So commit the document an edit returns as it is. A parent that puts its own copy in the Model after each edit (normalized, say) gets a fresh host, and a lost caret, on every keystroke.

When the new document continues the old one (another replica's change arriving, or the document a collaborative edit projects to), return patchTo(hostId, previous, next) from the same transition that puts it in the Model. It hands the host's key on from previous to next.document, which have to be the very objects the view rendered and will render, and patches the editor from the latest state it holds (what it has drawn, or a state waiting for an IME to finish) to next, so the host and every run the change did not touch keep their elements. next.selection becomes the browser selection, so pass the caret carried across the change (Replicated.resolve of an anchored one), or the caret is lost. A replacement that changes nothing drawn, as an exchange that only confirms edits already shown does, leaves the DOM and its selection alone. replaceChangeSet(previous, next) is the patch, in a transaction's terms: the runs whose text or marks differ and the blocks holding them, blocks whose runs moved in or out or changed order, blocks whose own fields changed, the nodes that came and went, and whether any block moved. An Edited OutMessage also carries the transactions the edit applied, which is what RichText.Replicated.translate reads.