Skip to content

Custom Reference Integration

Many deployments need to display a local catalog of references, highlight citations as authors select them, and sync back to an external service. This guide distills the moving parts so you can recreate the pattern in your own UI.

Data Flow

  1. Host app owns the reference list (from your API, CSL store, etc.).
  2. Editor emits events (editor-change, editor-selection-change) whenever the document or selection changes.
  3. Sidebar components render references, highlight matches, and dispatch drag/drop or click events back to the editor.

Step-by-Step

1. Render Your Reference Panel

Create a list component (plain DOM or framework) that exposes two methods:

  • render(references: Array<{ id: string; rawReference: string; mimeType?: string }>)
  • highlight(ids: string[])

Start with static markup and evolve it as needed; no special helpers are required from the package.

2. Listen for Document Updates

editor.addEventListener('editor-change', (event) => {
  const { doc, references } = event.detail;
  latestDoc = doc;
  panel.render(references ?? resolveReferences(doc));
});

resolveReferences is your hook to fetch or derive metadata (CSL JSON, raw citations, etc.) when you do not supply a references array alongside the snapshot.

3. Highlight References Based on Selection

Listen to editor-selection-change, extract citation IDs from the selection range, and pass them into your panel. Use getCitedReferenceIds from the library instead of walking the document manually:

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

editor.addEventListener('editor-selection-change', (event) => {
  const { from, to } = event.detail;
  const doc = editor.document; // live ProseMirror doc
  const ids = (from != null && to != null && from !== to)
    ? getCitedReferenceIds(doc, from, to)
    : getCitedReferenceIds(doc);
  panel.highlight(ids);
});

For a complete reference list example (including drag payloads), see sciflow-reference-list.

4. Support Drag-and-Drop Insertion

Use a consistent drag payload so the citation feature can consume it automatically:

transfer.setData('application/x-sciflow-reference', 'sidebar');
transfer.setData('application/json', JSON.stringify({
  type: 'reference',
  id: reference.id,
  text: reference.rawReference,
}));

The citation feature automatically consumes this payload via the drop handler in packages/editor/core/src/lib/features/citation/index.ts, calling runInsertCitation. Reuse the same MIME types so no extra work is required in the editor.

Drop behavior is context-aware:

  • Dropping onto empty space inserts a new citation at the current insertion point.
  • Dropping onto an existing parenthesized or bracketed expression converts the full expression to a citation (for example (Muller, 2025) or [222; 123]).
  • Dropping onto regular prose converts only the word under the drop target.
  • Dropping onto an existing citation augments citation source metadata; it does not replace the citation's visible text.

Optional: Programmatic Insertion

If you prefer buttons or menus to drag-and-drop:

const insertCitation = editor.commands?.commands?.insertCitation;
if (insertCitation) {
  insertCitation({
    items: [{ id: 'reference-1' }],
    text: '[1]',
    style: 'apa',
  });
}

Async lookups

Resolve metadata (authors, titles) before calling the command. The editor only stores what you send in text plus whatever you include in the node attributes.

Ghost Cursor

When the user moves focus to a sidebar panel (e.g. to click Cite), the editor's blinking text cursor disappears. The ghost cursor feature solves this by keeping a visual marker at the last known cursor position whenever the editor is unfocused.

Enabling ghost cursor

ghostCursorFeature is exported from @sciflow/editor-start. Add it to your feature list:

import {
  ghostCursorFeature,
  citationFeature,
  // ...other features
} from '@sciflow/editor-start';

await editor.configureFeatures([
  citationFeature,
  // ...
  ghostCursorFeature,
]);

Once active, the feature:

  • Renders a pulsing blue caret at the collapsed cursor position when the editor loses focus.
  • Renders a blue highlight over the selected range when the editor loses focus with a text selection.
  • Removes both decorations the moment the editor regains focus.

No extra CSS is required—the styles ship with @sciflow/editor-start.

How it works with cursor-aware buttons

The ghost cursor pairs with the cursorActive property on sciflow-reference-list (and the outline's Insert ref buttons). The editor preserves its ProseMirror selection state even when the DOM focus is elsewhere, so insertCitation will still land at the right position as long as cursorActive was set to true before the user moved to the sidebar.

User clicks in editor        → editor-selection-change → cursorActive = true
User clicks sidebar button   → mousedown preventDefault keeps cursor alive
                               editor loses DOM focus
                               ghostCursorFeature shows blue caret
User clicks Cite             → insertCitation() fires at saved position
User clicks Insert ref       → insertCrossReference() fires at saved position
New document loads           → editor-ready → cursorActive = false