Skip to content

Custom Document Outline

SciFlow already provides the primitives you need to build a table-of-contents sidebar:

  • collectDocumentOutline(doc, pmDoc?) – walks either the JSON snapshot or the live ProseMirror document and returns headings + citation lists. See packages/editor/start/demo/js/outline.js.
  • OutlinePanel – a ready-made DOM helper used by the demo sidebar.

Use this guide when you want to integrate the same behavior into your own UI.

When to Call collectDocumentOutline

Context Argument
Web component editor.doc (JSON) and editor.editorView?.state.doc (ProseMirror)
Core Editor instance editor.getDoc() and editor.getView()?.state.doc

Passing the live ProseMirror document enables accurate position data so outline clicks can set the selection.

Rendering Your Own Panel

import { collectDocumentOutline } from '@sciflow/editor-start/demo';

function renderOutline(editor, container) {
  const pmDoc = editor.editorView?.state.doc ?? null;
  const outline = collectDocumentOutline(editor.doc, pmDoc);

  container.innerHTML = '';
  outline.headings.forEach((heading) => {
    const item = document.createElement('li');
    item.textContent = heading.text || 'Untitled section';
    item.dataset.level = String(heading.level ?? 1);

    if (heading.position != null) {
      item.role = 'button';
      item.tabIndex = 0;
      item.addEventListener('click', () =>
        editor.commands?.commands?.setSelection?.(heading.position, { scroll: false }),
      );
    }

    container.appendChild(item);
  });
}

Copy the demo implementation

packages/editor/start/demo/js/outline.js contains a full-featured panel with keyboard support, metadata tags, and scroll handling. It’s safe to copy/paste as a starting point.

Scrolling Into View

The outline handler can call commands.setSelection(position, { scroll: false }) followed by commands.scrollIntoView() and commands.focus() to keep behavior consistent with the editor’s native navigation.

const runner = editor.commands;
if (runner?.commands?.setSelection) {
  const didSet = runner.commands.setSelection(position, { scroll: false });
  if (didSet && runner.commands.scrollIntoView) {
    runner.commands.scrollIntoView();
  }
  runner.commands.focus?.();
}

Re-render Triggers

  • editor-change – update the outline whenever the document structure changes.
  • Feature toggles – if you enable/disable heading support on the fly, re-run renderOutline after calling editor.configureFeatures(...).

Graceful degradation

If the editor view is not ready yet, collectDocumentOutline still returns heading data with position: null. In that state you can show the outline as read-only and enable click-to-jump later.