QTI Interactions
QTI Editor ships its interactions as focused subpath exports. Each interaction has its own package subpath and a matching register entrypoint, and the smallest useful integration is usually to import and register one interaction at a time.
These packages are closely coupled to our other open source library, qti-components. In practice, QTI Editor is responsible for authoring behavior and document structure, while qti-components supplies the underlying QTI web components that render and encapsulate the interaction behavior itself. That split keeps the integration framework agnostic while letting each interaction package stay small and explicit.
When you install @citolab/prose-qti, the matching @qti-components/* dependencies are pulled in automatically. You typically import only the interactions your product needs.
Available interactions
| Interaction | What it does | Package |
|---|---|---|
| Associate Interaction | Associating options from a shared set according to configured matching rules | @citolab/prose-qti/components/associate |
| Choice Interaction | Single- or multiple-choice questions with a prompt and selectable choices | @citolab/prose-qti/components/choice |
| Gap Match Interaction | Dragging or placing predefined gap texts into matching gaps in a passage | @citolab/prose-qti/components/gap-match |
| Hottext Interaction | Marking words or phrases inside a text passage as selectable response options | @citolab/prose-qti/components/hottext |
| Inline Choice Interaction | Dropdown choices embedded directly in a sentence | @citolab/prose-qti/components/inline-choice |
| Text Entry Interaction | Short free-text answers entered inline | @citolab/prose-qti/components/text-entry |
| Extended Text Interaction | Longer free-text answers in a multi-line response area | @citolab/prose-qti/components/extended-text |
| Match Interaction | Matching items across two sets | @citolab/prose-qti/components/match |
| Order Interaction | Ordering a list of choices into the correct sequence | @citolab/prose-qti/components/order |
| Select Point Interaction | Clicking a coordinate or hotspot on an image | @citolab/prose-qti/components/select-point |
Installing interactions
Install @citolab/prose-qti, then import each interaction’s descriptor, node spec, and register.js module explicitly:
pnpm add @citolab/prose-qtiimport { choiceInteractionDescriptor, qtiChoiceInteractionNodeSpec } from '@citolab/prose-qti/components/choice';import '@citolab/prose-qti/components/choice/register.js';Each interaction subpath exports:
*InteractionDescriptor—tagName,insertCommand,enterCommand,backspaceCommand,pluginFactories,attributePanelMetadata,nodeSpecsqti*InteractionNodeSpec— ProseMirror node spec (also reachable viadescriptor.nodeSpecs)register.js— side-effect module that defines the Lit custom element used by the node view
Shared building blocks (qti-prompt, qti-simple-choice, qti-simple-associable-choice, qti-simple-match-set, qti-gap, qti-gap-text) live under @citolab/prose-qti/components/shared and have their own per-component register.js modules.
Assembling
Collect the descriptors you want to support into a list and derive the schema, plugins, and attribute allowlist from them. See the TypeScript Integration guide for the full pattern.
import { chainCommands } from 'prosemirror-commands';import { keymap } from 'prosemirror-keymap';import { choiceInteractionDescriptor } from '@citolab/prose-qti/components/choice';import { textEntryInteractionDescriptor } from '@citolab/prose-qti/components/text-entry';// …import the rest
const descriptors = [ choiceInteractionDescriptor, textEntryInteractionDescriptor, // …];
const enterCommand = chainCommands(...descriptors.flatMap(d => d.enterCommand ?? []));const backspaceCommand = chainCommands(...descriptors.flatMap(d => d.backspaceCommand ?? []));
const qtiPlugins = [ keymap({ Enter: enterCommand, Backspace: backspaceCommand }), ...descriptors.flatMap(d => d.pluginFactories?.map(factory => factory()) ?? []),];Inserting an interaction
Each descriptor exposes an insertCommand — a standard ProseMirror command. Use it from your toolbar or programmatically:
import { choiceInteractionDescriptor } from '@citolab/prose-qti/components/choice';
choiceInteractionDescriptor.insertCommand!(view.state, view.dispatch, view);The TypeScript Integration guide shows how to build an Insert dropdown from the full descriptors list using prosemirror-menu.
Attributes
Each descriptor’s attributePanelMetadata declares which attributes are user-editable per node type. Common attributes:
| Attribute | Description |
|---|---|
response-identifier | Links the interaction to a response declaration |
max-choices | Maximum number of selectable options (choice interaction) |
expected-length | Character limit hint (text entry / extended text) |
case-sensitive | Whether the response match is case-sensitive (text entry) |
The boilerplate ships a small attributesPanelPlugin that renders these into a side panel; the TypeScript Integration guide walks through it.