@qti-editor/interaction-choice 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/attributes/choice-interaction-class-editor.d.ts +63 -0
  2. package/dist/attributes/choice-interaction-class-editor.d.ts.map +1 -0
  3. package/dist/attributes/choice-interaction-class-editor.js +124 -0
  4. package/dist/components/qti-choice-interaction/commands/convert-selection-to-choice.commands.d.ts +4 -0
  5. package/dist/components/qti-choice-interaction/commands/convert-selection-to-choice.commands.d.ts.map +1 -0
  6. package/dist/components/qti-choice-interaction/commands/convert-selection-to-choice.commands.js +169 -0
  7. package/dist/components/qti-choice-interaction/commands/insert-choice-interaction.commands.d.ts +17 -0
  8. package/dist/components/qti-choice-interaction/commands/insert-choice-interaction.commands.d.ts.map +1 -0
  9. package/dist/components/qti-choice-interaction/commands/insert-choice-interaction.commands.js +51 -0
  10. package/dist/components/qti-choice-interaction/qti-choice-interaction.commands.d.ts +8 -0
  11. package/dist/components/qti-choice-interaction/qti-choice-interaction.commands.d.ts.map +1 -0
  12. package/dist/components/qti-choice-interaction/qti-choice-interaction.commands.js +7 -0
  13. package/dist/components/qti-choice-interaction/qti-choice-interaction.compose.d.ts +3 -0
  14. package/dist/components/qti-choice-interaction/qti-choice-interaction.compose.d.ts.map +1 -0
  15. package/dist/components/qti-choice-interaction/qti-choice-interaction.compose.js +58 -0
  16. package/dist/components/qti-choice-interaction/qti-choice-interaction.d.ts +18 -0
  17. package/dist/components/qti-choice-interaction/qti-choice-interaction.d.ts.map +1 -0
  18. package/dist/components/qti-choice-interaction/qti-choice-interaction.js +85 -0
  19. package/dist/components/qti-choice-interaction/qti-choice-interaction.schema.d.ts +3 -0
  20. package/dist/components/qti-choice-interaction/qti-choice-interaction.schema.d.ts.map +1 -0
  21. package/dist/components/qti-choice-interaction/qti-choice-interaction.schema.js +39 -0
  22. package/dist/composer/handler.d.ts +3 -0
  23. package/dist/composer/handler.d.ts.map +1 -0
  24. package/dist/composer/handler.js +6 -0
  25. package/dist/composer/metadata.d.ts +27 -0
  26. package/dist/composer/metadata.d.ts.map +1 -0
  27. package/dist/composer/metadata.js +41 -0
  28. package/dist/descriptor.d.ts +36 -0
  29. package/dist/descriptor.d.ts.map +1 -0
  30. package/dist/descriptor.js +22 -0
  31. package/dist/extensions/correct-response-click.d.ts +24 -0
  32. package/dist/extensions/correct-response-click.d.ts.map +1 -0
  33. package/dist/extensions/correct-response-click.js +104 -0
  34. package/dist/extensions/index.d.ts +2 -0
  35. package/dist/extensions/index.d.ts.map +1 -0
  36. package/dist/extensions/index.js +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +9 -0
  40. package/package.json +47 -0
@@ -0,0 +1,63 @@
1
+ import type { AttributeFriendlyEditorDefinition } from '@qti-editor/interfaces';
2
+ export type ChoiceInteractionClassGroupId = 'labels' | 'labelsSuffix' | 'orientation' | 'inputControlHidden' | 'choicesStacking';
3
+ export interface ChoiceInteractionClassOption {
4
+ value: string;
5
+ label: string;
6
+ description?: string;
7
+ }
8
+ export interface ChoiceInteractionClassGroup {
9
+ id: ChoiceInteractionClassGroupId;
10
+ title: string;
11
+ description?: string;
12
+ selection: 'single' | 'boolean';
13
+ options: readonly ChoiceInteractionClassOption[];
14
+ }
15
+ export interface ChoiceInteractionClassState {
16
+ labels: string | null;
17
+ labelsSuffix: string | null;
18
+ orientation: string | null;
19
+ inputControlHidden: boolean;
20
+ choicesStacking: string | null;
21
+ unknownClasses: string[];
22
+ }
23
+ export interface ChoiceInteractionClassFriendlyEditorDefinition extends AttributeFriendlyEditorDefinition {
24
+ kind: 'choiceInteractionClass';
25
+ }
26
+ export declare const choiceInteractionClassGroups: readonly [{
27
+ readonly id: "labels";
28
+ readonly title: "Labels";
29
+ readonly description: "Choose how simple choices are labeled.";
30
+ readonly selection: "single";
31
+ readonly options: readonly ChoiceInteractionClassOption[];
32
+ }, {
33
+ readonly id: "labelsSuffix";
34
+ readonly title: "Label suffix";
35
+ readonly description: "Choose the suffix appended to each label.";
36
+ readonly selection: "single";
37
+ readonly options: readonly ChoiceInteractionClassOption[];
38
+ }, {
39
+ readonly id: "orientation";
40
+ readonly title: "Orientation";
41
+ readonly description: "Choose whether choices are laid out horizontally or vertically.";
42
+ readonly selection: "single";
43
+ readonly options: readonly ChoiceInteractionClassOption[];
44
+ }, {
45
+ readonly id: "inputControlHidden";
46
+ readonly title: "Input control";
47
+ readonly description: "Toggle visibility of the built-in input control.";
48
+ readonly selection: "boolean";
49
+ readonly options: readonly ChoiceInteractionClassOption[];
50
+ }, {
51
+ readonly id: "choicesStacking";
52
+ readonly title: "Stacking";
53
+ readonly description: "Choose how many columns choices are stacked into.";
54
+ readonly selection: "single";
55
+ readonly options: readonly ChoiceInteractionClassOption[];
56
+ }];
57
+ export declare const choiceInteractionClassFriendlyEditor: {
58
+ readonly attribute: "class";
59
+ readonly kind: "choiceInteractionClass";
60
+ };
61
+ export declare function parseChoiceInteractionClasses(classValue: string | null | undefined): ChoiceInteractionClassState;
62
+ export declare function serializeChoiceInteractionClasses(state: Partial<ChoiceInteractionClassState>): string | null;
63
+ //# sourceMappingURL=choice-interaction-class-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"choice-interaction-class-editor.d.ts","sourceRoot":"","sources":["../../src/attributes/choice-interaction-class-editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,wBAAwB,CAAC;AAEhF,MAAM,MAAM,6BAA6B,GACrC,QAAQ,GACR,cAAc,GACd,aAAa,GACb,oBAAoB,GACpB,iBAAiB,CAAC;AAEtB,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,6BAA6B,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,SAAS,4BAA4B,EAAE,CAAC;CAClD;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,8CACf,SAAQ,iCAAiC;IACzC,IAAI,EAAE,wBAAwB,CAAC;CAChC;AAgCD,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoCkB,CAAC;AAE5D,eAAO,MAAM,oCAAoC;;;CAGkB,CAAC;AAwBpE,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,2BAA2B,CAqB7B;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,OAAO,CAAC,2BAA2B,CAAC,GAC1C,MAAM,GAAG,IAAI,CAsBf"}
@@ -0,0 +1,124 @@
1
+ const LABEL_OPTIONS = [
2
+ { value: 'qti-labels-none', label: 'No labels' },
3
+ { value: 'qti-labels-decimal', label: 'Decimal labels' },
4
+ { value: 'qti-labels-lower-alpha', label: 'Lower alpha labels' },
5
+ { value: 'qti-labels-upper-alpha', label: 'Upper alpha labels' },
6
+ ];
7
+ const LABEL_SUFFIX_OPTIONS = [
8
+ { value: 'qti-labels-suffix-none', label: 'No suffix' },
9
+ { value: 'qti-labels-suffix-period', label: 'Period suffix' },
10
+ { value: 'qti-labels-suffix-parenthesis', label: 'Parenthesis suffix' },
11
+ ];
12
+ const ORIENTATION_OPTIONS = [
13
+ { value: 'qti-orientation-horizontal', label: 'Horizontal' },
14
+ { value: 'qti-orientation-vertical', label: 'Vertical' },
15
+ ];
16
+ const INPUT_CONTROL_OPTIONS = [
17
+ { value: 'qti-input-control-hidden', label: 'Hide input control' },
18
+ ];
19
+ const STACKING_OPTIONS = [
20
+ { value: 'qti-choices-stacking-1', label: '1 column' },
21
+ { value: 'qti-choices-stacking-2', label: '2 columns' },
22
+ { value: 'qti-choices-stacking-3', label: '3 columns' },
23
+ { value: 'qti-choices-stacking-4', label: '4 columns' },
24
+ { value: 'qti-choices-stacking-5', label: '5 columns' },
25
+ ];
26
+ export const choiceInteractionClassGroups = [
27
+ {
28
+ id: 'labels',
29
+ title: 'Labels',
30
+ description: 'Choose how simple choices are labeled.',
31
+ selection: 'single',
32
+ options: LABEL_OPTIONS,
33
+ },
34
+ {
35
+ id: 'labelsSuffix',
36
+ title: 'Label suffix',
37
+ description: 'Choose the suffix appended to each label.',
38
+ selection: 'single',
39
+ options: LABEL_SUFFIX_OPTIONS,
40
+ },
41
+ {
42
+ id: 'orientation',
43
+ title: 'Orientation',
44
+ description: 'Choose whether choices are laid out horizontally or vertically.',
45
+ selection: 'single',
46
+ options: ORIENTATION_OPTIONS,
47
+ },
48
+ {
49
+ id: 'inputControlHidden',
50
+ title: 'Input control',
51
+ description: 'Toggle visibility of the built-in input control.',
52
+ selection: 'boolean',
53
+ options: INPUT_CONTROL_OPTIONS,
54
+ },
55
+ {
56
+ id: 'choicesStacking',
57
+ title: 'Stacking',
58
+ description: 'Choose how many columns choices are stacked into.',
59
+ selection: 'single',
60
+ options: STACKING_OPTIONS,
61
+ },
62
+ ];
63
+ export const choiceInteractionClassFriendlyEditor = {
64
+ attribute: 'class',
65
+ kind: 'choiceInteractionClass',
66
+ };
67
+ const managedTokensByGroup = {
68
+ labels: new Set(LABEL_OPTIONS.map(option => option.value)),
69
+ labelsSuffix: new Set(LABEL_SUFFIX_OPTIONS.map(option => option.value)),
70
+ orientation: new Set(ORIENTATION_OPTIONS.map(option => option.value)),
71
+ inputControlHidden: new Set(INPUT_CONTROL_OPTIONS.map(option => option.value)),
72
+ choicesStacking: new Set(STACKING_OPTIONS.map(option => option.value)),
73
+ };
74
+ const managedTokens = new Set(Object.values(managedTokensByGroup).flatMap(groupTokens => Array.from(groupTokens)));
75
+ function tokenizeClassValue(classValue) {
76
+ if (!classValue)
77
+ return [];
78
+ return classValue
79
+ .split(/\s+/)
80
+ .map(token => token.trim())
81
+ .filter(Boolean)
82
+ .filter((token, index, tokens) => tokens.indexOf(token) === index);
83
+ }
84
+ export function parseChoiceInteractionClasses(classValue) {
85
+ const tokens = tokenizeClassValue(classValue);
86
+ const getLastManagedToken = (groupId) => {
87
+ let selected = null;
88
+ for (const token of tokens) {
89
+ if (managedTokensByGroup[groupId].has(token)) {
90
+ selected = token;
91
+ }
92
+ }
93
+ return selected;
94
+ };
95
+ return {
96
+ labels: getLastManagedToken('labels'),
97
+ labelsSuffix: getLastManagedToken('labelsSuffix'),
98
+ orientation: getLastManagedToken('orientation'),
99
+ inputControlHidden: tokens.includes('qti-input-control-hidden'),
100
+ choicesStacking: getLastManagedToken('choicesStacking'),
101
+ unknownClasses: tokens.filter(token => !managedTokens.has(token)),
102
+ };
103
+ }
104
+ export function serializeChoiceInteractionClasses(state) {
105
+ const tokens = [];
106
+ for (const token of state.unknownClasses ?? []) {
107
+ if (!token || managedTokens.has(token) || tokens.includes(token))
108
+ continue;
109
+ tokens.push(token);
110
+ }
111
+ const managedSelections = [
112
+ state.labels ?? null,
113
+ state.labelsSuffix ?? null,
114
+ state.orientation ?? null,
115
+ state.inputControlHidden ? 'qti-input-control-hidden' : null,
116
+ state.choicesStacking ?? null,
117
+ ];
118
+ for (const token of managedSelections) {
119
+ if (!token || tokens.includes(token))
120
+ continue;
121
+ tokens.push(token);
122
+ }
123
+ return tokens.length > 0 ? tokens.join(' ') : null;
124
+ }
@@ -0,0 +1,4 @@
1
+ import type { EditorView } from 'prosekit/pm/view';
2
+ export declare function canConvertFlatListToChoiceInteraction(view: EditorView): boolean;
3
+ export declare function convertFlatListToChoiceInteraction(view: EditorView): boolean;
4
+ //# sourceMappingURL=convert-selection-to-choice.commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert-selection-to-choice.commands.d.ts","sourceRoot":"","sources":["../../../../src/components/qti-choice-interaction/commands/convert-selection-to-choice.commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAiInD,wBAAgB,qCAAqC,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAmB/E;AAED,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAmD5E"}
@@ -0,0 +1,169 @@
1
+ function isFlatList(listNode, listType) {
2
+ let hasNestedList = false;
3
+ listNode.descendants((node, pos) => {
4
+ if (pos > 0 && node.type === listType) {
5
+ hasNestedList = true;
6
+ return false;
7
+ }
8
+ return !hasNestedList;
9
+ });
10
+ return !hasNestedList;
11
+ }
12
+ function isConvertibleRootBlock(node, schema) {
13
+ const paragraphType = schema.nodes.paragraph;
14
+ const listType = schema.nodes.list;
15
+ if (paragraphType && node.type === paragraphType) {
16
+ return 'paragraph';
17
+ }
18
+ if (listType && node.type === listType) {
19
+ const listKind = node.attrs?.kind;
20
+ if (!['bullet', 'ordered'].includes(listKind))
21
+ return null;
22
+ if (!isFlatList(node, listType))
23
+ return null;
24
+ return 'list';
25
+ }
26
+ return null;
27
+ }
28
+ function getSelectedConvertibleBlocks(view) {
29
+ const { state } = view;
30
+ const { selection } = state;
31
+ const schema = state.schema;
32
+ const blocks = [];
33
+ const seenPositions = new Set();
34
+ const addBlockIfConvertible = (node, start) => {
35
+ if (seenPositions.has(start))
36
+ return;
37
+ const kind = isConvertibleRootBlock(node, schema);
38
+ if (!kind)
39
+ return;
40
+ seenPositions.add(start);
41
+ blocks.push({
42
+ kind,
43
+ pos: start,
44
+ end: start + node.nodeSize,
45
+ node,
46
+ });
47
+ };
48
+ const getRootBlockAt = (pos) => {
49
+ const safePos = Math.max(0, Math.min(pos, state.doc.content.size));
50
+ const nodeAtPos = state.doc.nodeAt(safePos);
51
+ if (nodeAtPos?.isBlock) {
52
+ return {
53
+ node: nodeAtPos,
54
+ start: safePos,
55
+ };
56
+ }
57
+ const $pos = state.doc.resolve(safePos);
58
+ if ($pos.depth < 1) {
59
+ const after = $pos.nodeAfter;
60
+ if (after?.isBlock) {
61
+ return {
62
+ node: after,
63
+ start: $pos.pos,
64
+ };
65
+ }
66
+ return null;
67
+ }
68
+ return {
69
+ node: $pos.node(1),
70
+ start: $pos.start(1) - 1,
71
+ };
72
+ };
73
+ if (selection.empty) {
74
+ const rootBlock = getRootBlockAt(selection.from);
75
+ if (!rootBlock)
76
+ return blocks;
77
+ addBlockIfConvertible(rootBlock.node, rootBlock.start);
78
+ return blocks;
79
+ }
80
+ // Block-select plugin uses a custom "node-range" selection with explicit block ranges.
81
+ // Prefer those exact ranges to avoid text-range quirks across custom elements/shadow DOM.
82
+ const selectionJSON = selection.toJSON();
83
+ if (selectionJSON.type === 'node-range' && Array.isArray(selection.ranges)) {
84
+ for (const range of selection.ranges) {
85
+ const rootBlock = getRootBlockAt(range.$from.pos);
86
+ if (!rootBlock)
87
+ continue;
88
+ addBlockIfConvertible(rootBlock.node, rootBlock.start);
89
+ }
90
+ if (blocks.length > 0)
91
+ return blocks;
92
+ }
93
+ state.doc.forEach((node, offset) => {
94
+ const start = offset;
95
+ const end = start + node.nodeSize;
96
+ const intersectsSelection = end > selection.from && start < selection.to;
97
+ if (!intersectsSelection)
98
+ return;
99
+ addBlockIfConvertible(node, start);
100
+ });
101
+ return blocks;
102
+ }
103
+ function getChoiceInlineContent(block, schema) {
104
+ if (block.kind === 'paragraph') {
105
+ return block.node.content;
106
+ }
107
+ const paragraphType = schema.nodes.paragraph;
108
+ if (!paragraphType)
109
+ return null;
110
+ const firstChild = block.node.firstChild;
111
+ if (firstChild && firstChild.type === paragraphType) {
112
+ return firstChild.content;
113
+ }
114
+ return null;
115
+ }
116
+ export function canConvertFlatListToChoiceInteraction(view) {
117
+ const { state } = view;
118
+ const schema = state.schema;
119
+ const interactionType = schema.nodes.qtiChoiceInteraction;
120
+ const promptType = schema.nodes.qtiPrompt;
121
+ const promptParagraphType = schema.nodes.qtiPromptParagraph;
122
+ const choiceType = schema.nodes.qtiSimpleChoice;
123
+ const choiceParagraphType = schema.nodes.qtiSimpleChoiceParagraph;
124
+ if (!interactionType || !promptType || !promptParagraphType || !choiceType || !choiceParagraphType) {
125
+ return false;
126
+ }
127
+ const selectedBlocks = getSelectedConvertibleBlocks(view);
128
+ if (selectedBlocks.length === 0)
129
+ return false;
130
+ const firstBlock = selectedBlocks[0];
131
+ const $pos = state.doc.resolve(firstBlock.pos);
132
+ return $pos.parent.canReplaceWith($pos.index(), $pos.index() + 1, interactionType);
133
+ }
134
+ export function convertFlatListToChoiceInteraction(view) {
135
+ const { state } = view;
136
+ const schema = state.schema;
137
+ const interactionType = schema.nodes.qtiChoiceInteraction;
138
+ const promptType = schema.nodes.qtiPrompt;
139
+ const promptParagraphType = schema.nodes.qtiPromptParagraph;
140
+ const choiceType = schema.nodes.qtiSimpleChoice;
141
+ const choiceParagraphType = schema.nodes.qtiSimpleChoiceParagraph;
142
+ if (!interactionType || !promptType || !promptParagraphType || !choiceType || !choiceParagraphType) {
143
+ return false;
144
+ }
145
+ const selectedBlocks = getSelectedConvertibleBlocks(view);
146
+ if (selectedBlocks.length === 0)
147
+ return false;
148
+ const prompt = promptType.create(null, promptParagraphType.create(null, schema.text('Select one option')));
149
+ const choices = selectedBlocks.map(block => {
150
+ const inlineContent = getChoiceInlineContent(block, schema);
151
+ const paragraphContent = inlineContent ?? schema.text(block.node.textContent.trim() || 'Option');
152
+ return choiceType.create({ identifier: `SIMPLE_CHOICE_${crypto.randomUUID()}` }, choiceParagraphType.create(null, paragraphContent));
153
+ });
154
+ const interaction = interactionType.create({ responseIdentifier: `RESPONSE_${crypto.randomUUID()}`, maxChoices: 1 }, [prompt, ...choices]);
155
+ const firstBlock = selectedBlocks[0];
156
+ const $pos = state.doc.resolve(firstBlock.pos);
157
+ if (!$pos.parent.canReplaceWith($pos.index(), $pos.index() + 1, interactionType)) {
158
+ return false;
159
+ }
160
+ const tr = state.tr;
161
+ for (let i = selectedBlocks.length - 1; i >= 0; i -= 1) {
162
+ const block = selectedBlocks[i];
163
+ tr.delete(block.pos, block.end);
164
+ }
165
+ tr.insert(firstBlock.pos, interaction);
166
+ view.dispatch(tr);
167
+ view.focus();
168
+ return true;
169
+ }
@@ -0,0 +1,17 @@
1
+ import type { Command } from 'prosemirror-state';
2
+ /**
3
+ * Command to insert a choice interaction at the current selection
4
+ */
5
+ export declare const insertChoiceInteraction: Command;
6
+ /**
7
+ * Handles Enter inside qti-simple-choice paragraphs by inserting a new empty
8
+ * sibling qti-simple-choice directly after the current one.
9
+ */
10
+ export declare const insertSimpleChoiceOnEnter: Command;
11
+ /**
12
+ * Enter command chain for choice interactions.
13
+ * 1) Insert new simple choice when inside qti-simple-choice.
14
+ * 2) Fallback to regular block split behavior elsewhere.
15
+ */
16
+ export declare const qtiChoiceEnterCommand: Command;
17
+ //# sourceMappingURL=insert-choice-interaction.commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insert-choice-interaction.commands.d.ts","sourceRoot":"","sources":["../../../../src/components/qti-choice-interaction/commands/insert-choice-interaction.commands.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,OAoCrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,OAcvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,OAA8D,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { chainCommands, splitBlock } from 'prosemirror-commands';
2
+ import { createInsertSiblingOnEnterCommand } from '@qti-editor/interaction-shared/commands/enter.js';
3
+ import { createInsertBlockInteractionCommand } from '@qti-editor/interaction-shared/commands/insert.js';
4
+ /**
5
+ * Command to insert a choice interaction at the current selection
6
+ */
7
+ export const insertChoiceInteraction = (state, dispatch) => {
8
+ return createInsertBlockInteractionCommand({
9
+ createNode: currentState => {
10
+ const { schema } = currentState;
11
+ const promptType = schema.nodes.qtiPrompt;
12
+ const promptParagraphType = schema.nodes.qtiPromptParagraph;
13
+ const choiceType = schema.nodes.qtiSimpleChoice;
14
+ const choiceParagraphType = schema.nodes.qtiSimpleChoiceParagraph;
15
+ const interactionType = schema.nodes.qtiChoiceInteraction;
16
+ if (!promptType || !promptParagraphType || !choiceType || !choiceParagraphType || !interactionType) {
17
+ return null;
18
+ }
19
+ const responseIdentifier = `RESPONSE_${crypto.randomUUID()}`;
20
+ const prompt = promptType.create(null, promptParagraphType.create(null, schema.text('Which option is correct?')));
21
+ const choices = [
22
+ choiceType.create({ identifier: `SIMPLE_CHOICE_${crypto.randomUUID()}` }, choiceParagraphType.create(null, schema.text('Option A'))),
23
+ choiceType.create({ identifier: `SIMPLE_CHOICE_${crypto.randomUUID()}` }, choiceParagraphType.create(null, schema.text('Option B'))),
24
+ choiceType.create({ identifier: `SIMPLE_CHOICE_${crypto.randomUUID()}` }, choiceParagraphType.create(null, schema.text('Option C')))
25
+ ];
26
+ return interactionType.create({ responseIdentifier, maxChoices: 1 }, [prompt, ...choices]);
27
+ },
28
+ selectionOffset: 2,
29
+ })(state, dispatch);
30
+ };
31
+ /**
32
+ * Handles Enter inside qti-simple-choice paragraphs by inserting a new empty
33
+ * sibling qti-simple-choice directly after the current one.
34
+ */
35
+ export const insertSimpleChoiceOnEnter = (state, dispatch) => {
36
+ const choiceType = state.schema.nodes.qtiSimpleChoice;
37
+ const paragraphType = state.schema.nodes.qtiSimpleChoiceParagraph;
38
+ if (!choiceType || !paragraphType)
39
+ return false;
40
+ return createInsertSiblingOnEnterCommand({
41
+ ancestorNodeName: 'qtiSimpleChoice',
42
+ selectionOffset: 2,
43
+ createSiblingNode: () => choiceType.create({ identifier: `SIMPLE_CHOICE_${crypto.randomUUID()}` }, paragraphType.create()),
44
+ })(state, dispatch);
45
+ };
46
+ /**
47
+ * Enter command chain for choice interactions.
48
+ * 1) Insert new simple choice when inside qti-simple-choice.
49
+ * 2) Fallback to regular block split behavior elsewhere.
50
+ */
51
+ export const qtiChoiceEnterCommand = chainCommands(insertSimpleChoiceOnEnter, splitBlock);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * QTI Choice Interaction Commands
3
+ *
4
+ * Backward-compatible command barrel that re-exports insert and convert commands.
5
+ */
6
+ export { insertChoiceInteraction, insertSimpleChoiceOnEnter, qtiChoiceEnterCommand } from './commands/insert-choice-interaction.commands.js';
7
+ export { canConvertFlatListToChoiceInteraction, convertFlatListToChoiceInteraction } from './commands/convert-selection-to-choice.commands.js';
8
+ //# sourceMappingURL=qti-choice-interaction.commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-choice-interaction.commands.d.ts","sourceRoot":"","sources":["../../../src/components/qti-choice-interaction/qti-choice-interaction.commands.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,EACtB,MAAM,kDAAkD,CAAC;AAE1D,OAAO,EACL,qCAAqC,EACrC,kCAAkC,EACnC,MAAM,oDAAoD,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * QTI Choice Interaction Commands
3
+ *
4
+ * Backward-compatible command barrel that re-exports insert and convert commands.
5
+ */
6
+ export { insertChoiceInteraction, insertSimpleChoiceOnEnter, qtiChoiceEnterCommand } from './commands/insert-choice-interaction.commands.js';
7
+ export { canConvertFlatListToChoiceInteraction, convertFlatListToChoiceInteraction } from './commands/convert-selection-to-choice.commands.js';
@@ -0,0 +1,3 @@
1
+ import type { InteractionComposeResult } from '@qti-editor/interaction-shared/composer/types.js';
2
+ export declare function composeChoiceInteractionElement(sourceElement: Element, xmlDoc: Document): InteractionComposeResult;
3
+ //# sourceMappingURL=qti-choice-interaction.compose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-choice-interaction.compose.d.ts","sourceRoot":"","sources":["../../../src/components/qti-choice-interaction/qti-choice-interaction.compose.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAmB,wBAAwB,EAAkC,MAAM,kDAAkD,CAAC;AAclJ,wBAAgB,+BAA+B,CAAC,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAG,wBAAwB,CAgDlH"}
@@ -0,0 +1,58 @@
1
+ import { choiceInteractionComposerMetadata } from '../../composer/metadata.js';
2
+ function toFiniteNumber(value, fallback) {
3
+ if (value == null || value.trim().length === 0)
4
+ return fallback;
5
+ const parsed = Number(value);
6
+ return Number.isFinite(parsed) ? parsed : fallback;
7
+ }
8
+ function toNonEmptyString(value) {
9
+ if (!value)
10
+ return null;
11
+ const trimmed = value.trim();
12
+ return trimmed.length > 0 ? trimmed : null;
13
+ }
14
+ export function composeChoiceInteractionElement(sourceElement, xmlDoc) {
15
+ const metadata = choiceInteractionComposerMetadata;
16
+ const warnings = [];
17
+ const normalizedElement = xmlDoc.importNode(sourceElement, true);
18
+ const responseIdentifier = toNonEmptyString(sourceElement.getAttribute('response-identifier'));
19
+ const correctResponse = toNonEmptyString(sourceElement.getAttribute('correct-response'));
20
+ const maxChoices = toFiniteNumber(sourceElement.getAttribute('max-choices'), 1);
21
+ const minChoices = toFiniteNumber(sourceElement.getAttribute('min-choices'), 0);
22
+ const editorOnlyAttributes = [...metadata.editorOnlyAttributes];
23
+ editorOnlyAttributes.forEach(attr => normalizedElement.removeAttribute(attr));
24
+ normalizedElement.setAttribute('max-choices', String(maxChoices));
25
+ if (minChoices > 0) {
26
+ normalizedElement.setAttribute('min-choices', String(minChoices));
27
+ }
28
+ else {
29
+ normalizedElement.removeAttribute('min-choices');
30
+ }
31
+ // QTI spec: max-choices=0 means unlimited, max-choices=1 means single selection
32
+ const isMultiple = maxChoices !== 1;
33
+ let responseDeclaration;
34
+ if (!responseIdentifier) {
35
+ warnings.push({
36
+ code: 'MISSING_RESPONSE_IDENTIFIER',
37
+ message: 'qti-choice-interaction is missing response-identifier; declaration will be skipped.',
38
+ tagName: metadata.tagName,
39
+ });
40
+ }
41
+ else {
42
+ responseDeclaration = {
43
+ identifier: responseIdentifier,
44
+ cardinality: isMultiple ? 'multiple' : 'single',
45
+ baseType: 'identifier',
46
+ correctResponse: correctResponse ?? undefined,
47
+ sourceTag: metadata.tagName,
48
+ };
49
+ }
50
+ return {
51
+ normalizedElement,
52
+ responseDeclaration,
53
+ responseProcessingTemplate: metadata.responseProcessingTemplate,
54
+ responseProcessingKind: metadata.responseProcessing.internalKind,
55
+ editorOnlyAttributes,
56
+ warnings,
57
+ };
58
+ }
@@ -0,0 +1,18 @@
1
+ import { Interaction } from '@qti-editor/interaction-shared/components/interaction.js';
2
+ export type Orientation = 'horizontal' | 'vertical' | undefined;
3
+ declare const ChoiceInteractionBase: typeof Interaction;
4
+ export declare class QtiChoiceInteractionEdit extends ChoiceInteractionBase {
5
+ #private;
6
+ static get styles(): import("lit").CSSResult[];
7
+ minChoices: number;
8
+ maxChoices: number;
9
+ classes: 'qti-orientation-vertical' | 'qti-orientation-horizontal' | undefined;
10
+ protected _internals: ElementInternals;
11
+ constructor();
12
+ connectedCallback(): void;
13
+ disconnectedCallback(): void;
14
+ updated(changedProperties: Map<string, unknown>): void;
15
+ render(): import("lit").TemplateResult<1>;
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=qti-choice-interaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-choice-interaction.d.ts","sourceRoot":"","sources":["../../../src/components/qti-choice-interaction/qti-choice-interaction.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,0DAA0D,CAAC;AAKvF,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,UAAU,GAAG,SAAS,CAAC;AAChE,QAAA,MAAM,qBAAqB,EAAwD,OAAO,WAAW,CAAC;AAEtG,qBAAa,wBAAyB,SAAQ,qBAAqB;;IACjE,WAAoB,MAAM,8BASzB;IAGM,UAAU,SAAK;IAGf,UAAU,SAAK;IAGf,OAAO,EAAE,0BAA0B,GAAG,4BAA4B,GAAG,SAAS,CAAC;IAEtF,SAAS,CAAC,UAAU,EAAE,gBAAgB,CAAC;;IAQvC,iBAAiB,IAAI,IAAI;IAOzB,oBAAoB,IAAI,IAAI;IAMnB,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAiB/C,MAAM;CAGhB"}
@@ -0,0 +1,85 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
13
+ if (kind === "m") throw new TypeError("Private method is not writable");
14
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
15
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
16
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
17
+ };
18
+ var _QtiChoiceInteractionEdit_instances, _QtiChoiceInteractionEdit_mutationObserver, _QtiChoiceInteractionEdit_updateChoices;
19
+ import { css, html } from 'lit';
20
+ import { property } from 'lit/decorators.js';
21
+ import { Interaction } from '@qti-editor/interaction-shared/components/interaction.js';
22
+ import styles from '@qti-components/choice-interaction/styles';
23
+ import { VocabularyMixin } from '@qti-components/interactions-core';
24
+ const ChoiceInteractionBase = VocabularyMixin(Interaction, 'qti-simple-choice');
25
+ export class QtiChoiceInteractionEdit extends ChoiceInteractionBase {
26
+ static get styles() {
27
+ return [
28
+ styles,
29
+ css `
30
+ :host {
31
+ white-space: nowrap;
32
+ }
33
+ `
34
+ ];
35
+ }
36
+ constructor() {
37
+ super();
38
+ _QtiChoiceInteractionEdit_instances.add(this);
39
+ this.minChoices = 0;
40
+ this.maxChoices = 1;
41
+ _QtiChoiceInteractionEdit_mutationObserver.set(this, null);
42
+ this._internals = this.attachInternals();
43
+ }
44
+ connectedCallback() {
45
+ super.connectedCallback();
46
+ __classPrivateFieldGet(this, _QtiChoiceInteractionEdit_instances, "m", _QtiChoiceInteractionEdit_updateChoices).call(this);
47
+ __classPrivateFieldSet(this, _QtiChoiceInteractionEdit_mutationObserver, new MutationObserver(() => __classPrivateFieldGet(this, _QtiChoiceInteractionEdit_instances, "m", _QtiChoiceInteractionEdit_updateChoices).call(this)), "f");
48
+ __classPrivateFieldGet(this, _QtiChoiceInteractionEdit_mutationObserver, "f").observe(this, { childList: true, subtree: true });
49
+ }
50
+ disconnectedCallback() {
51
+ super.disconnectedCallback();
52
+ __classPrivateFieldGet(this, _QtiChoiceInteractionEdit_mutationObserver, "f")?.disconnect();
53
+ __classPrivateFieldSet(this, _QtiChoiceInteractionEdit_mutationObserver, null, "f");
54
+ }
55
+ updated(changedProperties) {
56
+ super.updated(changedProperties);
57
+ if (changedProperties.has('maxChoices')) {
58
+ __classPrivateFieldGet(this, _QtiChoiceInteractionEdit_instances, "m", _QtiChoiceInteractionEdit_updateChoices).call(this);
59
+ }
60
+ }
61
+ render() {
62
+ return html `<slot part="prompt" name="prompt"></slot><slot part="slot"></slot>`;
63
+ }
64
+ }
65
+ _QtiChoiceInteractionEdit_mutationObserver = new WeakMap(), _QtiChoiceInteractionEdit_instances = new WeakSet(), _QtiChoiceInteractionEdit_updateChoices = function _QtiChoiceInteractionEdit_updateChoices() {
66
+ this._internals.role = this.maxChoices === 1 ? 'radiogroup' : null;
67
+ const role = this.maxChoices === 1 ? 'radio' : 'checkbox';
68
+ this.querySelectorAll('qti-simple-choice').forEach((choice) => {
69
+ choice.internals.role = role;
70
+ choice.internals.states.delete(role === 'radio' ? 'checkbox' : 'radio');
71
+ choice.internals.states.add(role);
72
+ });
73
+ };
74
+ __decorate([
75
+ property({ type: Number, attribute: 'min-choices' })
76
+ ], QtiChoiceInteractionEdit.prototype, "minChoices", void 0);
77
+ __decorate([
78
+ property({ type: Number, attribute: 'max-choices' })
79
+ ], QtiChoiceInteractionEdit.prototype, "maxChoices", void 0);
80
+ __decorate([
81
+ property({ type: String, attribute: 'class' })
82
+ ], QtiChoiceInteractionEdit.prototype, "classes", void 0);
83
+ if (!customElements.get('qti-choice-interaction')) {
84
+ customElements.define('qti-choice-interaction', QtiChoiceInteractionEdit);
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { NodeSpec } from 'prosemirror-model';
2
+ export declare const qtiChoiceInteractionNodeSpec: NodeSpec;
3
+ //# sourceMappingURL=qti-choice-interaction.schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-choice-interaction.schema.d.ts","sourceRoot":"","sources":["../../../src/components/qti-choice-interaction/qti-choice-interaction.schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEjE,eAAO,MAAM,4BAA4B,EAAE,QAkC1C,CAAC"}
@@ -0,0 +1,39 @@
1
+ export const qtiChoiceInteractionNodeSpec = {
2
+ group: 'block',
3
+ content: 'qtiPrompt qtiSimpleChoice+',
4
+ attrs: {
5
+ maxChoices: { default: 0 },
6
+ class: { default: null },
7
+ correctResponse: { default: null },
8
+ responseIdentifier: { default: null }
9
+ },
10
+ parseDOM: [
11
+ {
12
+ tag: 'qti-choice-interaction',
13
+ getAttrs: (node) => {
14
+ if (!(node instanceof HTMLElement))
15
+ return {};
16
+ const maxChoices = node.getAttribute('max-choices');
17
+ const className = node.getAttribute('class');
18
+ return {
19
+ maxChoices: maxChoices ? parseInt(maxChoices, 10) : 0,
20
+ class: className || null,
21
+ correctResponse: node.getAttribute('correct-response'),
22
+ responseIdentifier: node.getAttribute('response-identifier')
23
+ };
24
+ }
25
+ }
26
+ ],
27
+ toDOM(node) {
28
+ const attrs = { 'max-choices': String(node.attrs.maxChoices) };
29
+ if (node.attrs.class)
30
+ attrs.class = node.attrs.class;
31
+ if (node.attrs.correctResponse)
32
+ attrs['correct-response'] = node.attrs.correctResponse;
33
+ if (node.attrs.responseIdentifier)
34
+ attrs['response-identifier'] = node.attrs.responseIdentifier;
35
+ return ['qti-choice-interaction', attrs, 0];
36
+ },
37
+ defining: true,
38
+ isolating: true
39
+ };
@@ -0,0 +1,3 @@
1
+ import type { InteractionComposerHandler } from '@qti-editor/interaction-shared/composer/types.js';
2
+ export declare const choiceComposerHandler: InteractionComposerHandler;
3
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/composer/handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kDAAkD,CAAC;AAEnG,eAAO,MAAM,qBAAqB,EAAE,0BAGnC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { composeChoiceInteractionElement } from '../components/qti-choice-interaction/qti-choice-interaction.compose.js';
2
+ import { CHOICE_INTERACTION_TAG } from './metadata.js';
3
+ export const choiceComposerHandler = {
4
+ tagName: CHOICE_INTERACTION_TAG,
5
+ compose: composeChoiceInteractionElement,
6
+ };
@@ -0,0 +1,27 @@
1
+ export declare const MATCH_CORRECT_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct";
2
+ export declare const CHOICE_INTERACTION_TAG: "qti-choice-interaction";
3
+ export declare const CHOICE_INTERACTION_NODE_TYPE: "qtiChoiceInteraction";
4
+ export declare const choiceInteractionComposerMetadata: {
5
+ tagName: "qti-choice-interaction";
6
+ nodeTypeName: "qtiChoiceInteraction";
7
+ responseProcessingTemplate: string;
8
+ responseProcessing: {
9
+ templateUri: string;
10
+ internalKind: "match_correct";
11
+ internalSourceXml: string;
12
+ };
13
+ editorOnlyAttributes: string[];
14
+ userEditableAttributes: string[];
15
+ };
16
+ export declare const choiceNodeAttributePanelMetadataByNodeTypeName: {
17
+ [x: string]: {
18
+ nodeTypeName: "qtiChoiceInteraction";
19
+ editableAttributes: string[];
20
+ hiddenAttributes: string[];
21
+ friendlyEditors: {
22
+ readonly attribute: "class";
23
+ readonly kind: "choiceInteractionClass";
24
+ }[];
25
+ };
26
+ };
27
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/composer/metadata.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,uEAAuE,CAAC;AAmB3G,eAAO,MAAM,sBAAsB,EAAG,wBAAiC,CAAC;AACxE,eAAO,MAAM,4BAA4B,EAAG,sBAA+B,CAAC;AAE5E,eAAO,MAAM,iCAAiC;;;;;;;;;;;CAYP,CAAC;AAExC,eAAO,MAAM,8CAA8C;;;;;;;;;;CAOL,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { choiceInteractionClassFriendlyEditor } from '../attributes/choice-interaction-class-editor.js';
2
+ export const MATCH_CORRECT_TEMPLATE = 'https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct';
3
+ const MATCH_CORRECT_INTERNAL_TEMPLATE = `
4
+ <qti-response-condition>
5
+ <qti-response-if>
6
+ <qti-match>
7
+ <qti-variable identifier="$responseIdentifier"/>
8
+ <qti-correct identifier="$responseIdentifier"/>
9
+ </qti-match>
10
+ <qti-set-outcome-value identifier="SCORE">
11
+ <qti-sum>
12
+ <qti-variable identifier="SCORE"/>
13
+ <qti-base-value base-type="float">1</qti-base-value>
14
+ </qti-sum>
15
+ </qti-set-outcome-value>
16
+ </qti-response-if>
17
+ </qti-response-condition>
18
+ `;
19
+ export const CHOICE_INTERACTION_TAG = 'qti-choice-interaction';
20
+ export const CHOICE_INTERACTION_NODE_TYPE = 'qtiChoiceInteraction';
21
+ export const choiceInteractionComposerMetadata = {
22
+ tagName: CHOICE_INTERACTION_TAG,
23
+ nodeTypeName: CHOICE_INTERACTION_NODE_TYPE,
24
+ responseProcessingTemplate: MATCH_CORRECT_TEMPLATE,
25
+ responseProcessing: {
26
+ templateUri: MATCH_CORRECT_TEMPLATE,
27
+ internalKind: 'match_correct',
28
+ internalSourceXml: MATCH_CORRECT_INTERNAL_TEMPLATE,
29
+ },
30
+ editorOnlyAttributes: ['class'],
31
+ // maxChoices and correctResponse are set by clicking choices, not edited directly
32
+ userEditableAttributes: ['class'],
33
+ };
34
+ export const choiceNodeAttributePanelMetadataByNodeTypeName = {
35
+ [CHOICE_INTERACTION_NODE_TYPE.toLowerCase()]: {
36
+ nodeTypeName: CHOICE_INTERACTION_NODE_TYPE,
37
+ editableAttributes: choiceInteractionComposerMetadata.userEditableAttributes,
38
+ hiddenAttributes: ['class'],
39
+ friendlyEditors: [choiceInteractionClassFriendlyEditor],
40
+ },
41
+ };
@@ -0,0 +1,36 @@
1
+ export declare const choiceInteractionDescriptor: {
2
+ tagName: string;
3
+ nodeTypeName: string;
4
+ nodeSpecs: {
5
+ name: string;
6
+ spec: import("prosemirror-model").NodeSpec;
7
+ }[];
8
+ insertCommand: import("prosemirror-state").Command;
9
+ keyboardShortcut: string;
10
+ enterCommand: import("prosemirror-state").Command;
11
+ composerMetadata: {
12
+ tagName: "qti-choice-interaction";
13
+ nodeTypeName: "qtiChoiceInteraction";
14
+ responseProcessingTemplate: string;
15
+ responseProcessing: {
16
+ templateUri: string;
17
+ internalKind: "match_correct";
18
+ internalSourceXml: string;
19
+ };
20
+ editorOnlyAttributes: string[];
21
+ userEditableAttributes: string[];
22
+ };
23
+ composerHandler: import("@qti-editor/interfaces").InteractionComposerHandler;
24
+ attributePanelMetadata: {
25
+ [x: string]: {
26
+ nodeTypeName: "qtiChoiceInteraction";
27
+ editableAttributes: string[];
28
+ hiddenAttributes: string[];
29
+ friendlyEditors: {
30
+ readonly attribute: "class";
31
+ readonly kind: "choiceInteractionClass";
32
+ }[];
33
+ };
34
+ };
35
+ };
36
+ //# sourceMappingURL=descriptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptor.d.ts","sourceRoot":"","sources":["../src/descriptor.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgBP,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { qtiPromptNodeSpec, qtiPromptParagraphNodeSpec, qtiSimpleChoiceNodeSpec, qtiSimpleChoiceParagraphNodeSpec, } from '@qti-editor/interaction-shared';
2
+ import { insertChoiceInteraction, insertSimpleChoiceOnEnter } from './components/qti-choice-interaction/qti-choice-interaction.commands.js';
3
+ import { qtiChoiceInteractionNodeSpec } from './components/qti-choice-interaction/qti-choice-interaction.schema.js';
4
+ import { choiceInteractionComposerMetadata, choiceNodeAttributePanelMetadataByNodeTypeName } from './composer/metadata.js';
5
+ import { choiceComposerHandler } from './composer/handler.js';
6
+ export const choiceInteractionDescriptor = {
7
+ tagName: 'qti-choice-interaction',
8
+ nodeTypeName: 'qtiChoiceInteraction',
9
+ nodeSpecs: [
10
+ { name: 'qtiChoiceInteraction', spec: qtiChoiceInteractionNodeSpec },
11
+ { name: 'qtiPrompt', spec: qtiPromptNodeSpec },
12
+ { name: 'qtiPromptParagraph', spec: qtiPromptParagraphNodeSpec },
13
+ { name: 'qtiSimpleChoice', spec: qtiSimpleChoiceNodeSpec },
14
+ { name: 'qtiSimpleChoiceParagraph', spec: qtiSimpleChoiceParagraphNodeSpec },
15
+ ],
16
+ insertCommand: insertChoiceInteraction,
17
+ keyboardShortcut: 'Mod-Shift-q',
18
+ enterCommand: insertSimpleChoiceOnEnter,
19
+ composerMetadata: choiceInteractionComposerMetadata,
20
+ composerHandler: choiceComposerHandler,
21
+ attributePanelMetadata: choiceNodeAttributePanelMetadataByNodeTypeName,
22
+ };
@@ -0,0 +1,24 @@
1
+ import { type Extension } from 'prosekit/core';
2
+ /**
3
+ * Event name for correct response changes at the interaction level.
4
+ * Dispatched when selections change to update the ProseMirror document.
5
+ */
6
+ export declare const QTI_CORRECT_RESPONSE_CHANGE_EVENT = "qti:correct-response:change";
7
+ /**
8
+ * Detail payload for the change event.
9
+ */
10
+ export interface QtiCorrectResponseChangeDetail {
11
+ maxChoices: number;
12
+ correctResponse: string | null;
13
+ interactionElement: HTMLElement;
14
+ }
15
+ /**
16
+ * Creates a ProseMirror extension that handles correct response clicks.
17
+ *
18
+ * When a qti-simple-choice is clicked (on part="ch"), this extension:
19
+ * 1. Aggregates all selected choices in the parent interaction
20
+ * 2. Computes maxChoices (1 for single, 0 for multiple)
21
+ * 3. Updates the interaction node's attributes
22
+ */
23
+ export declare function defineCorrectResponseClickExtension(): Extension;
24
+ //# sourceMappingURL=correct-response-click.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correct-response-click.d.ts","sourceRoot":"","sources":["../../src/extensions/correct-response-click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAW7D;;;GAGG;AACH,eAAO,MAAM,iCAAiC,gCAAgC,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kBAAkB,EAAE,WAAW,CAAC;CACjC;AAqDD;;;;;;;GAOG;AACH,wBAAgB,mCAAmC,IAAI,SAAS,CAmD/D"}
@@ -0,0 +1,104 @@
1
+ import { definePlugin } from 'prosekit/core';
2
+ import { Plugin, PluginKey } from 'prosekit/pm/state';
3
+ import { QTI_CORRECT_RESPONSE_TOGGLE_EVENT, } from '@qti-editor/interaction-shared';
4
+ const correctResponseClickPluginKey = new PluginKey('correct-response-click');
5
+ /**
6
+ * Event name for correct response changes at the interaction level.
7
+ * Dispatched when selections change to update the ProseMirror document.
8
+ */
9
+ export const QTI_CORRECT_RESPONSE_CHANGE_EVENT = 'qti:correct-response:change';
10
+ /**
11
+ * Finds the parent qti-choice-interaction element from a choice element.
12
+ */
13
+ function findParentInteraction(element) {
14
+ return element.closest('qti-choice-interaction');
15
+ }
16
+ /**
17
+ * Gets all selected choice identifiers from an interaction element.
18
+ */
19
+ function getSelectedIdentifiers(interactionElement) {
20
+ const choices = interactionElement.querySelectorAll('qti-simple-choice');
21
+ const selected = [];
22
+ choices.forEach((choice) => {
23
+ const choiceElement = choice;
24
+ if (choiceElement.selected) {
25
+ const identifier = choice.getAttribute('identifier');
26
+ if (identifier) {
27
+ selected.push(identifier);
28
+ }
29
+ }
30
+ });
31
+ return selected;
32
+ }
33
+ /**
34
+ * Finds the ProseMirror node position for an interaction element.
35
+ */
36
+ function findInteractionNodePos(view, interactionElement) {
37
+ const { state } = view;
38
+ let foundPos = null;
39
+ state.doc.descendants((node, pos) => {
40
+ if (foundPos !== null)
41
+ return false; // Already found, stop searching
42
+ if (node.type.name === 'qtiChoiceInteraction') {
43
+ // Check if this node corresponds to the DOM element
44
+ const domNode = view.nodeDOM(pos);
45
+ if (domNode === interactionElement || (domNode && domNode.contains(interactionElement))) {
46
+ foundPos = pos;
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ });
52
+ return foundPos;
53
+ }
54
+ /**
55
+ * Creates a ProseMirror extension that handles correct response clicks.
56
+ *
57
+ * When a qti-simple-choice is clicked (on part="ch"), this extension:
58
+ * 1. Aggregates all selected choices in the parent interaction
59
+ * 2. Computes maxChoices (1 for single, 0 for multiple)
60
+ * 3. Updates the interaction node's attributes
61
+ */
62
+ export function defineCorrectResponseClickExtension() {
63
+ return definePlugin(() => new Plugin({
64
+ key: correctResponseClickPluginKey,
65
+ view(view) {
66
+ const handleToggle = (event) => {
67
+ const choiceElement = event.target;
68
+ // Find the parent interaction
69
+ const interactionElement = findParentInteraction(choiceElement);
70
+ if (!interactionElement)
71
+ return;
72
+ // Get all selected identifiers
73
+ const selectedIdentifiers = getSelectedIdentifiers(interactionElement);
74
+ // Compute maxChoices and correctResponse
75
+ const maxChoices = selectedIdentifiers.length <= 1 ? 1 : 0;
76
+ const correctResponse = selectedIdentifiers.length > 0
77
+ ? selectedIdentifiers.join(',')
78
+ : null;
79
+ // Find the node position
80
+ const pos = findInteractionNodePos(view, interactionElement);
81
+ if (pos === null)
82
+ return;
83
+ // Update the node attributes
84
+ const { state, dispatch } = view;
85
+ const node = state.doc.nodeAt(pos);
86
+ if (!node)
87
+ return;
88
+ const nextAttrs = {
89
+ ...node.attrs,
90
+ maxChoices,
91
+ correctResponse,
92
+ };
93
+ dispatch(state.tr.setNodeMarkup(pos, undefined, nextAttrs));
94
+ };
95
+ // Listen for toggle events on the editor DOM
96
+ view.dom.addEventListener(QTI_CORRECT_RESPONSE_TOGGLE_EVENT, handleToggle);
97
+ return {
98
+ destroy() {
99
+ view.dom.removeEventListener(QTI_CORRECT_RESPONSE_TOGGLE_EVENT, handleToggle);
100
+ },
101
+ };
102
+ },
103
+ }));
104
+ }
@@ -0,0 +1,2 @@
1
+ export { defineCorrectResponseClickExtension, QTI_CORRECT_RESPONSE_CHANGE_EVENT, type QtiCorrectResponseChangeDetail, } from './correct-response-click.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,EACnC,iCAAiC,EACjC,KAAK,8BAA8B,GACpC,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1 @@
1
+ export { defineCorrectResponseClickExtension, QTI_CORRECT_RESPONSE_CHANGE_EVENT, } from './correct-response-click.js';
@@ -0,0 +1,10 @@
1
+ export * from './components/qti-choice-interaction/qti-choice-interaction.js';
2
+ export * from './components/qti-choice-interaction/qti-choice-interaction.schema.js';
3
+ export * from './components/qti-choice-interaction/qti-choice-interaction.commands.js';
4
+ export * from './components/qti-choice-interaction/qti-choice-interaction.compose.js';
5
+ export * from './composer/metadata.js';
6
+ export * from './composer/handler.js';
7
+ export * from './attributes/choice-interaction-class-editor.js';
8
+ export * from './extensions/index.js';
9
+ export * from './descriptor.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+DAA+D,CAAC;AAC9E,cAAc,sEAAsE,CAAC;AACrF,cAAc,wEAAwE,CAAC;AACvF,cAAc,uEAAuE,CAAC;AAEtF,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iDAAiD,CAAC;AAEhE,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './components/qti-choice-interaction/qti-choice-interaction.js';
2
+ export * from './components/qti-choice-interaction/qti-choice-interaction.schema.js';
3
+ export * from './components/qti-choice-interaction/qti-choice-interaction.commands.js';
4
+ export * from './components/qti-choice-interaction/qti-choice-interaction.compose.js';
5
+ export * from './composer/metadata.js';
6
+ export * from './composer/handler.js';
7
+ export * from './attributes/choice-interaction-class-editor.js';
8
+ export * from './extensions/index.js';
9
+ export * from './descriptor.js';
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@qti-editor/interaction-choice",
3
+ "description": "QTI choice interaction schemas, components, commands, and composer",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./*": {
14
+ "types": "./dist/*",
15
+ "default": "./dist/*"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@qti-components/base": "^1.4.1",
20
+ "@qti-components/choice-interaction": "^1.1.0",
21
+ "@qti-components/interactions": "^1.10.1",
22
+ "@qti-components/interactions-core": "^1.1.0",
23
+ "@qti-components/utilities": "^1.3.1",
24
+ "@qti-editor/interfaces": "0.1.0",
25
+ "@qti-editor/interaction-shared": "0.1.0"
26
+ },
27
+ "peerDependencies": {
28
+ "lit": "^3.3.1"
29
+ },
30
+ "devDependencies": {
31
+ "lit": "^3.3.1",
32
+ "prosemirror-commands": "^1.7.1",
33
+ "prosemirror-model": "^1.25.4",
34
+ "prosemirror-state": "^1.4.4"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit",
44
+ "build": "tsc",
45
+ "clean": "rm -rf dist"
46
+ }
47
+ }