
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Overview
Complete the ordering interaction plugin end-to-end: XML parsing, XML assembly, validation, a useOrderingInteraction composable built on useInteraction, and a working OrderingInteractionEditor.vue.
This task depends on the choice and text-entry interaction plugins being in place (for useInteraction, generateRandomSlug, and defineInteraction).
Complexity: Medium
Target branch: unstable
Context
The ordering interaction maps to <qti-order-interaction> and covers one question type:
- ordering — the author enters items in the correct order; the learner sees them in the initial display order (which may be shuffled by a delivery engine) and must reorder them.
The ordering interaction is a block-level element — it stands alone in the item body, consistent with the choice interaction.
The response declaration uses cardinality="ordered" and base-type="identifier", listing choice identifiers in the correct order.
State shape
Defined via JSDoc in interactions/ordering/parse.js. parse produces (and buildXML consumes) this flat state object:
/**
* @typedef {object} OrderingItem
* @property {string} id - QTI identifier, e.g. "order_xlqTuVoq"
* @property {string} content - HTML content of the <qti-simple-choice>
* @property {boolean} fixed - Whether this item is fixed in place (round-trip only; not editable in UI)
*/
/**
* @typedef {object} OrderingState
* @property {string} responseIdentifier - Response identifier attribute
* @property {string} prompt - HTML content of <qti-prompt>; default ""
* @property {OrderingItem[]} items - Items in the CORRECT order
* @property {string} orientation - From orientation attribute; default "vertical"
* @property {number|undefined} maxChoices - From max-choices attribute; undefined when absent
* @property {number|undefined} minChoices - From min-choices attribute; undefined when absent
* @property {boolean} shuffle - From shuffle attribute; default true for new items
*/
QTI XML reference
Official spec example (§3.2.10, IMS Global BPIG):
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
identifier="QTI3-order" title="Order Interaction Example"
time-dependent="false" xml:lang="en-US">
<qti-response-declaration identifier="RESPONSE"
cardinality="ordered" base-type="identifier">
<qti-correct-response>
<qti-value>DriverC</qti-value>
<qti-value>DriverA</qti-value>
<qti-value>DriverB</qti-value>
</qti-correct-response>
</qti-response-declaration>
<qti-outcome-declaration identifier="SCORE"
cardinality="single" base-type="float"/>
<qti-item-body>
<p>The following F1 drivers finished on the podium in the first ever Grand Prix of Bahrain.</p>
<qti-order-interaction response-identifier="RESPONSE" orientation="horizontal">
<qti-prompt>
<p>Rearrange them into the correct finishing order.</p>
</qti-prompt>
<qti-simple-choice identifier="DriverA">Rubens Barrichello</qti-simple-choice>
<qti-simple-choice identifier="DriverB">Jenson Button</qti-simple-choice>
<qti-simple-choice identifier="DriverC">Michael Schumacher</qti-simple-choice>
</qti-order-interaction>
</qti-item-body>
<qti-response-processing
template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml"/>
</qti-assessment-item>
Studio body XML only (what the interaction block stores):
<qti-order-interaction response-identifier="RESPONSE" orientation="vertical">
<qti-prompt><p>Arrange the planets in order from closest to farthest from the Sun.</p></qti-prompt>
<qti-simple-choice identifier="order_abc12345">Mercury</qti-simple-choice>
<qti-simple-choice identifier="order_def67890">Venus</qti-simple-choice>
<qti-simple-choice identifier="order_ghi11223">Earth</qti-simple-choice>
<qti-simple-choice identifier="order_jkl44556">Mars</qti-simple-choice>
</qti-order-interaction>
Response declaration:
<qti-response-declaration identifier="RESPONSE" cardinality="ordered" base-type="identifier">
<qti-correct-response>
<qti-value>order_abc12345</qti-value>
<qti-value>order_def67890</qti-value>
<qti-value>order_ghi11223</qti-value>
<qti-value>order_jkl44556</qti-value>
</qti-correct-response>
</qti-response-declaration>
Rules:
cardinality is always "ordered" — this is what makes it an ordering interaction.
base-type is always "identifier".
<qti-correct-response> lists item identifiers in the correct order — the order the learner must match.
shuffle is supported and controls whether the delivery engine randomizes the initial display order. While the QTI spec defaults this to false, Studio defaults to true for newly authored items.
orientation defaults to "vertical" when absent.
- Items without an
identifier attribute must be assigned a generated slug (generateRandomSlug('order')).
The Change
1. interactions/ordering/parse.js
Export parseOrderingInteraction(bodyXml, responseDeclarations) → OrderingState:
- Parse
bodyXml with parseXML.
- Read
orientation, max-choices, min-choices from the <qti-order-interaction> element's attributes. Default orientation to "vertical"; leave maxChoices/minChoices as undefined when absent.
- Extract
<qti-prompt> inner HTML → prompt via the shared getPromptHTML helper.
- Collect
<qti-simple-choice> elements → items array (each with id, content, fixed).
- Parse the response declaration string with
QTIDeclaration.fromXML to extract the ordered list of correct identifiers. Reorder items to match the <qti-correct-response> identifier sequence so the editor always shows the canonical correct order.
Export buildOrderingInteractionXML(state, questionType, declarationSchema) → { bodyXml: string, responseDeclarations: string[] }:
- Serialize
state.prompt HTML as the <qti-prompt> child.
- Render each item in
state.items order as a <qti-simple-choice>.
- Emit
orientation attribute; omit max-choices/min-choices when they are undefined.
- Build a
<qti-response-declaration> using QTIDeclaration with cardinality="ordered", base-type="identifier", and a <qti-correct-response> listing item identifiers in state.items order.
- Return
{ bodyXml, responseDeclarations: [declarationXml] }.
Export _defaultState() for use in tests and the descriptor. Crucial: It must seed the items array with two empty items (each with a generated ID). This ensures the component renders correctly for a new item, prevents an empty <qti-correct-response/> schema error, and satisfies the "requires at least 2 items" rule out of the box.
2. interactions/ordering/validate.js
Export validateOrderingInteraction(state) → ValidationError[]:
| Rule |
Condition |
| Prompt required |
state.prompt is empty or whitespace-only |
| Empty item content |
Any item has empty or whitespace-only content |
| Too few items |
Fewer than 2 items in state.items (spec: at least 2 to be useful) |
3. interactions/ordering/OrderingInteractionDescriptor.js
Define the descriptor class following the same pattern as ChoiceInteractionDescriptor:
type: QtiInteraction.ORDER ('qti-order-interaction')
placement: 'block'
questionTypes: [QuestionType.ORDERING]
matches(el): el.tagName.toLowerCase() === QtiInteraction.ORDER
getQuestionType(): always returns QuestionType.ORDERING
getResponseDeclarationSchema(): returns { baseType: BaseType.IDENTIFIER, cardinality: Cardinality.ORDERED }
parse(bodyXml, responseDeclarations): delegates to parseOrderingInteraction
buildXML(state, questionType): delegates to buildOrderingInteractionXML
validate(state): delegates to validateOrderingInteraction
convertsFrom: []
Export the singleton orderingInteractionDescriptor.
4. interactions/ordering/index.js
import defineInteraction from '../defineInteraction';
import OrderingInteractionEditor from './OrderingInteractionEditor.vue';
import { orderingInteractionDescriptor } from './OrderingInteractionDescriptor';
export default defineInteraction(orderingInteractionDescriptor, OrderingInteractionEditor);
5. interactions/index.js — register the descriptor
Add the ordering descriptor to the descriptors array alongside choiceDescriptor and textEntryDescriptor.
6. constants.js & qtiEditorStrings.js
- Add
ORDERING: 'ordering' to the QuestionType freeze object in constants.js.
- Add an explicit translation key (e.g.,
orderingLabel$: 'Ordering') to qtiEditorStrings.js and map it explicitly in the UI string lookup. Do not dynamically concatenate translation keys.
7. composables/useOrderingInteraction.js
Build on useInteraction and expose state-mutation methods:
addItem() — appends a new item with generateRandomSlug('order') id and empty content.
removeItem(id) — no-op when only one item remains.
moveItemUp(id) / moveItemDown(id) — swap array positions; no-op at boundaries.
setItemContent(id, html) — updates the item's content field.
setShuffle(shuffle) — toggles the shuffle boolean in the state.
8. interactions/ordering/OrderingInteractionEditor.vue
A Vue SFC wiring the composable to the UI.
Props:
props: {
interaction: Object, // { bodyXml, responseDeclarations }
questionType: String, // 'ordering'
mode: String, // 'edit' | 'view'
showAnswers: Boolean,
teleportTargetId: String,
}
Emits: 'update:interaction' — { bodyXml, responseDeclarations } whenever state changes.
UI behaviour:
- Renders the prompt RTE at the top.
- Renders a "Correct order" header with subtext "Learners will see these shuffled" above the items list.
- Renders a list of item rows. Each row shows:
- A drag handle (6 dots) on the far left.
- A green numbered box indicating its position in the correct order (1, 2, 3...).
- A content RTE (same
TipTapEditor used in ChoiceInteractionEditor). Must have :readonly="mode !== 'edit'".
- Move-up (
^), move-down (v), and delete (X) icon buttons on the right (delete is disabled when only one item remains). A11y: Tooltips/aria-labels must be parameterized with the item's position (e.g., "Delete option {number}").
- Use the shared
AddListItemButton component for the "Add option" button appended below the list.
- Focus Management: When
addItem is clicked, use Vue template refs (e.g., an array of refs via v-for) to set focus on the newly added item's text editor. Do not use document.getElementById.
- In view mode with
showAnswers: true: items shown in correct order, read-only, under an "Answers" header. The drag handles, up/down/delete buttons, and "Add option" button are hidden.
- In view mode with
showAnswers: false: items hidden.
- Emits
'update:interaction'. Bug prevention: The watcher triggering this emit must include an early return (if (props.mode !== 'edit') return;) so view mode doesn't swallow or mutate XML state.
- Calls
runValidation() on prompt blur and each item content RTE blur. Validation UX: Do not run validation immediately on addItem(), as it creates a harsh UX by turning a pristine input red.
- Note on
shuffle: The design currently assumes shuffle is active. We will use the standard AnswerSettings component (via teleport) to provide a toggle for the shuffle attribute, pending final design confirmation on how it should be presented.
Acceptance Criteria
parse.js
validate.js
Descriptor + registry
useOrderingInteraction
OrderingInteractionEditor.vue
Testing
References
- QTI 3.0 spec §3.2.10 Order Interaction: IMS Global BPIG and QTI 3.0 XSD schema (
OrderInteractionDType) which explicitly defines the shuffle attribute.
- Architecture:
shared/views/QTIEditor/ — see interactions/choice/ as the reference implementation
- Existing reordering utilities:
composables/useChoiceInteraction.js → moveChoiceUp / moveChoiceDown
- Backend QTI reference:
contentcuration/utils/assessment/qti/archive.py
AI usage
I used Claude (Antigravity) to draft this issue from design decisions and the QTI editor architecture.
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Overview
Complete the ordering interaction plugin end-to-end: XML parsing, XML assembly, validation, a
useOrderingInteractioncomposable built onuseInteraction, and a workingOrderingInteractionEditor.vue.This task depends on the choice and text-entry interaction plugins being in place (for
useInteraction,generateRandomSlug, anddefineInteraction).Complexity: Medium
Target branch:
unstableContext
The ordering interaction maps to
<qti-order-interaction>and covers one question type:The ordering interaction is a block-level element — it stands alone in the item body, consistent with the choice interaction.
The response declaration uses
cardinality="ordered"andbase-type="identifier", listing choice identifiers in the correct order.State shape
Defined via JSDoc in
interactions/ordering/parse.js.parseproduces (andbuildXMLconsumes) this flat state object:QTI XML reference
Official spec example (§3.2.10, IMS Global BPIG):
Studio body XML only (what the interaction block stores):
Response declaration:
Rules:
cardinalityis always"ordered"— this is what makes it an ordering interaction.base-typeis always"identifier".<qti-correct-response>lists item identifiers in the correct order — the order the learner must match.shuffleis supported and controls whether the delivery engine randomizes the initial display order. While the QTI spec defaults this to false, Studio defaults totruefor newly authored items.orientationdefaults to"vertical"when absent.identifierattribute must be assigned a generated slug (generateRandomSlug('order')).The Change
1.
interactions/ordering/parse.jsExport
parseOrderingInteraction(bodyXml, responseDeclarations)→OrderingState:bodyXmlwithparseXML.orientation,max-choices,min-choicesfrom the<qti-order-interaction>element's attributes. Defaultorientationto"vertical"; leavemaxChoices/minChoicesasundefinedwhen absent.<qti-prompt>inner HTML →promptvia the sharedgetPromptHTMLhelper.<qti-simple-choice>elements →itemsarray (each withid,content,fixed).QTIDeclaration.fromXMLto extract the ordered list of correct identifiers. Reorderitemsto match the<qti-correct-response>identifier sequence so the editor always shows the canonical correct order.Export
buildOrderingInteractionXML(state, questionType, declarationSchema)→{ bodyXml: string, responseDeclarations: string[] }:state.promptHTML as the<qti-prompt>child.state.itemsorder as a<qti-simple-choice>.orientationattribute; omitmax-choices/min-choiceswhen they areundefined.<qti-response-declaration>usingQTIDeclarationwithcardinality="ordered",base-type="identifier", and a<qti-correct-response>listing item identifiers instate.itemsorder.{ bodyXml, responseDeclarations: [declarationXml] }.Export
_defaultState()for use in tests and the descriptor. Crucial: It must seed theitemsarray with two empty items (each with a generated ID). This ensures the component renders correctly for a new item, prevents an empty<qti-correct-response/>schema error, and satisfies the "requires at least 2 items" rule out of the box.2.
interactions/ordering/validate.jsExport
validateOrderingInteraction(state)→ValidationError[]:state.promptis empty or whitespace-onlycontentstate.items(spec: at least 2 to be useful)3.
interactions/ordering/OrderingInteractionDescriptor.jsDefine the descriptor class following the same pattern as
ChoiceInteractionDescriptor:type:QtiInteraction.ORDER('qti-order-interaction')placement:'block'questionTypes:[QuestionType.ORDERING]matches(el):el.tagName.toLowerCase() === QtiInteraction.ORDERgetQuestionType(): always returnsQuestionType.ORDERINGgetResponseDeclarationSchema(): returns{ baseType: BaseType.IDENTIFIER, cardinality: Cardinality.ORDERED }parse(bodyXml, responseDeclarations): delegates toparseOrderingInteractionbuildXML(state, questionType): delegates tobuildOrderingInteractionXMLvalidate(state): delegates tovalidateOrderingInteractionconvertsFrom:[]Export the singleton
orderingInteractionDescriptor.4.
interactions/ordering/index.js5.
interactions/index.js— register the descriptorAdd the ordering descriptor to the
descriptorsarray alongsidechoiceDescriptorandtextEntryDescriptor.6.
constants.js&qtiEditorStrings.jsORDERING: 'ordering'to theQuestionTypefreeze object inconstants.js.orderingLabel$: 'Ordering') toqtiEditorStrings.jsand map it explicitly in the UI string lookup. Do not dynamically concatenate translation keys.7.
composables/useOrderingInteraction.jsBuild on
useInteractionand expose state-mutation methods:addItem()— appends a new item withgenerateRandomSlug('order')id and empty content.removeItem(id)— no-op when only one item remains.moveItemUp(id)/moveItemDown(id)— swap array positions; no-op at boundaries.setItemContent(id, html)— updates the item'scontentfield.setShuffle(shuffle)— toggles the shuffle boolean in the state.8.
interactions/ordering/OrderingInteractionEditor.vueA Vue SFC wiring the composable to the UI.
Props:
Emits:
'update:interaction'—{ bodyXml, responseDeclarations }whenever state changes.UI behaviour:
TipTapEditorused inChoiceInteractionEditor). Must have:readonly="mode !== 'edit'".^), move-down (v), and delete (X) icon buttons on the right (delete is disabled when only one item remains). A11y: Tooltips/aria-labels must be parameterized with the item's position (e.g.,"Delete option {number}").AddListItemButtoncomponent for the "Add option" button appended below the list.addItemis clicked, use Vue template refs (e.g., an array of refs viav-for) to set focus on the newly added item's text editor. Do not usedocument.getElementById.showAnswers: true: items shown in correct order, read-only, under an "Answers" header. The drag handles, up/down/delete buttons, and "Add option" button are hidden.showAnswers: false: items hidden.'update:interaction'. Bug prevention: The watcher triggering this emit must include an early return (if (props.mode !== 'edit') return;) so view mode doesn't swallow or mutate XML state.runValidation()on prompt blur and each item content RTE blur. Validation UX: Do not run validation immediately onaddItem(), as it creates a harsh UX by turning a pristine input red.shuffle: The design currently assumes shuffle is active. We will use the standardAnswerSettingscomponent (via teleport) to provide a toggle for theshuffleattribute, pending final design confirmation on how it should be presented.Acceptance Criteria
parse.jsparseOrderingInteraction(bodyXml, responseDeclarations)returns anOrderingStatewith correctprompt,items, andorientationvalues.state.itemsare ordered to match the<qti-correct-response>identifier sequence when a declaration is present.identifierattribute are assigned a generatedorder_<8chars>slug.maxChoicesandminChoicesare preserved from the XML when present;undefinedwhen absent.buildOrderingInteractionXMLreturns{ bodyXml, responseDeclarations }that round-trips:parse(buildXML(state))produces an equivalent state.buildOrderingInteractionXMLemits identifiers instate.itemsorder inside<qti-correct-response>.orientationdefaults to"vertical"when absent;max-choices/min-choicesare omitted from the serialized XML whenundefined.shuffleattribute is read during parsing and emitted correctly bybuildOrderingInteractionXML.validate.jsPROMPT_REQUIREDwhenstate.promptis empty or whitespace-only.EMPTY_ANSWER_CONTENTfor each item with empty or whitespace-onlycontent.TOO_FEW_CHOICESwhen fewer than 2 items are present.Descriptor + registry
OrderingInteractionDescriptorpassesdefineInteractionvalidation without errors.getQuestionType()always returnsQuestionType.ORDERING.interactions/index.jsregistry.QuestionType.ORDERINGis added toconstants.js.useOrderingInteractionaddItem()appends a new item with a generatedorder_<8 chars>identifier and empty content.removeItem(id)is a no-op when only one item remains.moveItemUp(id)/moveItemDown(id)swap positions correctly; no-op at first/last position.setItemContent(id, html)updates only the targeted item'scontent.errorsstarts empty;runValidation()populates it viavalidateOrderingInteraction.OrderingInteractionEditor.vueshowAnswers: true: items are shown in correct order, read-only.showAnswers: false: items are hidden.update:interactionwhenever state changes, with an early return blocking emits in view mode.runValidation()on blur, never immediately onaddItem().<qti-order-interaction>XML renders pre-filled; reordering items updates the live XML.Testing
parseOrderingInteraction: round-trip for ordering items, correct-response ordering,orientationdefault, absentmax-choices/min-choices→undefined, andshuffleattribute preservation.buildOrderingInteractionXML: identifier order in declaration matchesstate.itemsorder;shuffleattribute emitted correctly.validateOrderingInteraction: each error condition covered, valid state returns[].useOrderingInteraction: each mutation method produces the expected state change.OrderingInteractionEditor.spec.js: edit mode rendering, view mode (showAnswers on/off), validation display,update:interactionemit on mutation.References
OrderInteractionDType) which explicitly defines theshuffleattribute.shared/views/QTIEditor/— seeinteractions/choice/as the reference implementationcomposables/useChoiceInteraction.js→moveChoiceUp/moveChoiceDowncontentcuration/utils/assessment/qti/archive.pyAI usage
I used Claude (Antigravity) to draft this issue from design decisions and the QTI editor architecture.