@qti-components/match-interaction 1.1.4 → 2.0.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.
@@ -0,0 +1,6 @@
1
+ import { QtiMatchInteraction } from './qti-match-interaction';
2
+ export declare const elements: readonly [{
3
+ readonly tag: "qti-match-interaction";
4
+ readonly ctor: typeof QtiMatchInteraction;
5
+ }];
6
+ export { QtiMatchInteraction };
@@ -0,0 +1,4 @@
1
+ import { QtiMatchInteraction } from './qti-match-interaction';
2
+ export const elements = [{ tag: 'qti-match-interaction', ctor: QtiMatchInteraction }];
3
+ export { QtiMatchInteraction };
4
+ //# sourceMappingURL=elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAE,GAAG,EAAE,uBAAuB,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAU,CAAC;AAE/F,OAAO,EAAE,mBAAmB,EAAE,CAAC","sourcesContent":["import { QtiMatchInteraction } from './qti-match-interaction';\n\nexport const elements = [{ tag: 'qti-match-interaction', ctor: QtiMatchInteraction }] as const;\n\nexport { QtiMatchInteraction };\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shadow-DOM styles for the tabular layout of qti-match-interaction
3
+ * (class="qti-match-tabular"). The two qti-simple-match-set children are
4
+ * slotted into named slots and participate in a CSS grid as light-DOM
5
+ * row/column headers via display: contents. Drag-drop mode is untouched.
6
+ */
7
+ declare const _default: import("lit").CSSResult[];
8
+ export default _default;
@@ -0,0 +1,69 @@
1
+ import { css } from 'lit';
2
+ import { boxSizing } from '@qti-components/base';
3
+ /**
4
+ * Shadow-DOM styles for the tabular layout of qti-match-interaction
5
+ * (class="qti-match-tabular"). The two qti-simple-match-set children are
6
+ * slotted into named slots and participate in a CSS grid as light-DOM
7
+ * row/column headers via display: contents. Drag-drop mode is untouched.
8
+ */
9
+ export default [
10
+ boxSizing,
11
+ css `
12
+ :host(.qti-match-tabular) slot:not([hidden]) {
13
+ display: contents;
14
+ }
15
+ :host(.qti-match-tabular) [part='grid'] {
16
+ display: grid;
17
+ /* Clips the cell rules — which are the grid's own background showing through a 1px gap — to the
18
+ theme's border-radius. */
19
+ overflow: hidden;
20
+ width: fit-content;
21
+ max-width: 100%;
22
+ grid-template-columns: minmax(min-content, max-content) repeat(var(--qti-match-cols, 1), auto);
23
+ grid-template-rows: auto repeat(var(--qti-match-rows, 1), auto);
24
+ }
25
+ /* Subgrid wrappers carry the slotted choices and the input cells into the
26
+ outer grid's tracks. This structure is shared with the editor's tabular
27
+ template so both can inherit the same theme CSS. No per-choice inline
28
+ style writes are needed — auto-flow + subgrid handle placement. */
29
+ :host(.qti-match-tabular) [part='cols-wrap'] {
30
+ grid-column: 2 / -1;
31
+ grid-row: 1;
32
+ display: grid;
33
+ grid-template-columns: subgrid;
34
+ }
35
+ :host(.qti-match-tabular) [part='rows-wrap'] {
36
+ grid-column: 1;
37
+ grid-row: 2 / -1;
38
+ display: grid;
39
+ grid-template-rows: subgrid;
40
+ }
41
+ :host(.qti-match-tabular) [part='checkbox-grid'] {
42
+ grid-column: 2 / -1;
43
+ grid-row: 2 / -1;
44
+ display: grid;
45
+ grid-template-columns: subgrid;
46
+ grid-template-rows: subgrid;
47
+ }
48
+ :host(.qti-match-tabular) ::slotted(qti-simple-match-set) {
49
+ display: contents;
50
+ }
51
+
52
+ /*
53
+ * The radio/checkbox box centres its own mark. qti-simple-choice.styles.ts does the same for
54
+ * the non-tabular control; here the cells are plain spans in this shadow root, so the rule
55
+ * belongs to this file.
56
+ *
57
+ * The theme's 'form' mixin used to carry 'display: grid; place-content: center', which centred
58
+ * both — from a document stylesheet, overriding qti-simple-choice's own flex centring. The mixin
59
+ * keeps the box's size and paint; the box itself is laid out here.
60
+ */
61
+ :host(.qti-match-tabular) [part~='control'] {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ flex-shrink: 0;
66
+ }
67
+ `
68
+ ];
69
+ //# sourceMappingURL=qti-match-interaction-tabular.styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-match-interaction-tabular.styles.js","sourceRoot":"","sources":["../src/qti-match-interaction-tabular.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD;;;;;GAKG;AACH,eAAe;IACb,SAAS;IACT,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDF;CACF,CAAC","sourcesContent":["import { css } from 'lit';\n\nimport { boxSizing } from '@qti-components/base';\n\n/**\n * Shadow-DOM styles for the tabular layout of qti-match-interaction\n * (class=\"qti-match-tabular\"). The two qti-simple-match-set children are\n * slotted into named slots and participate in a CSS grid as light-DOM\n * row/column headers via display: contents. Drag-drop mode is untouched.\n */\nexport default [\n boxSizing,\n css`\n :host(.qti-match-tabular) slot:not([hidden]) {\n display: contents;\n }\n :host(.qti-match-tabular) [part='grid'] {\n display: grid;\n /* Clips the cell rules — which are the grid's own background showing through a 1px gap — to the\n theme's border-radius. */\n overflow: hidden;\n width: fit-content;\n max-width: 100%;\n grid-template-columns: minmax(min-content, max-content) repeat(var(--qti-match-cols, 1), auto);\n grid-template-rows: auto repeat(var(--qti-match-rows, 1), auto);\n }\n /* Subgrid wrappers carry the slotted choices and the input cells into the\n outer grid's tracks. This structure is shared with the editor's tabular\n template so both can inherit the same theme CSS. No per-choice inline\n style writes are needed — auto-flow + subgrid handle placement. */\n :host(.qti-match-tabular) [part='cols-wrap'] {\n grid-column: 2 / -1;\n grid-row: 1;\n display: grid;\n grid-template-columns: subgrid;\n }\n :host(.qti-match-tabular) [part='rows-wrap'] {\n grid-column: 1;\n grid-row: 2 / -1;\n display: grid;\n grid-template-rows: subgrid;\n }\n :host(.qti-match-tabular) [part='checkbox-grid'] {\n grid-column: 2 / -1;\n grid-row: 2 / -1;\n display: grid;\n grid-template-columns: subgrid;\n grid-template-rows: subgrid;\n }\n :host(.qti-match-tabular) ::slotted(qti-simple-match-set) {\n display: contents;\n }\n\n /*\n * The radio/checkbox box centres its own mark. qti-simple-choice.styles.ts does the same for\n * the non-tabular control; here the cells are plain spans in this shadow root, so the rule\n * belongs to this file.\n *\n * The theme's 'form' mixin used to carry 'display: grid; place-content: center', which centred\n * both — from a document stylesheet, overriding qti-simple-choice's own flex centring. The mixin\n * keeps the box's size and paint; the box itself is laid out here.\n */\n :host(.qti-match-tabular) [part~='control'] {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n }\n `\n];\n"]}
@@ -1,10 +1,64 @@
1
1
  import { Interaction } from '@qti-components/base';
2
- import type { CSSResultGroup } from 'lit';
2
+ import type { CSSResultGroup, PropertyValues } from 'lit';
3
3
  import type { QtiSimpleAssociableChoice } from '@qti-components/interactions-core/elements/qti-simple-associable-choice';
4
4
  declare const QtiMatchInteraction_base: (abstract new (...args: any[]) => import("@qti-components/interactions-core/mixins/drag-drop-observables").DragDropSlottedSortable) & (abstract new (...args: any[]) => import("@qti-components/interactions-core/mixins/drag-drop-observables").DragDropSlotted) & typeof Interaction;
5
+ /**
6
+ * Match interaction: candidates pair items from two sets, either as drag-and-drop or as a tabular grid.
7
+ *
8
+ * @customElement qti-match-interaction
9
+ *
10
+ * @attr {string} response-identifier - Required. Identifier of the bound response variable.
11
+ * @attr {number} [max-associations=0] - Maximum pairings across the whole interaction; `0`
12
+ * means unlimited. QTI's own default is `1`; this element defaults to unlimited because a
13
+ * literal `1` breaks any multi-pair match item. See plans/interaction-attribute-spec-conformance.md.
14
+ * @attr {number} [min-associations=1] - Minimum pairings for a valid response.
15
+ * @attr {boolean} [shuffle=false] - Requests shuffling of both match sets. Applied by the
16
+ * transform pipeline (`qti-transformers`), not by this element.
17
+ * @attr {'qti-match-tabular'|'qti-choices-top'|'qti-choices-bottom'|'qti-choices-left'|'qti-choices-right'} class -
18
+ * QTI shared presentation vocabulary. `qti-match-tabular` renders a checkbox/radio grid
19
+ * instead of drag-and-drop; the `qti-choices-*` values position the source set.
20
+ * @attr {boolean} [auto-size-dropzones=false] - Extension, not QTI. Sizes every drop target to
21
+ * the widest chip so placement does not reflow the layout. The mixin default is `true`; this
22
+ * interaction is the one that overrides it to `false`, because a match target is a category card
23
+ * rather than a socket (see the field below and DROP-SIZING.md §1).
24
+ * @attr {boolean} [disable-animations=false] - Extension, not QTI. Disables the FLIP move
25
+ * animation.
26
+ *
27
+ * @slot prompt - The prompt shown above the interaction.
28
+ * @slot match-cols - Column headers (tabular mode).
29
+ * @slot match-rows - Row headers (tabular mode).
30
+ * @slot - Default slot for the two match sets (drag-and-drop mode).
31
+ *
32
+ * @csspart grid - The tabular grid container.
33
+ * @csspart corner - The top-left corner cell of the grid.
34
+ * @csspart cols-wrap - Wrapper row for the column headers.
35
+ * @csspart c-header - The column header slot wrapper.
36
+ * @csspart rows-wrap - Wrapper column for the row headers.
37
+ * @csspart r-header - The row header slot wrapper.
38
+ * @csspart checkbox-grid - The interactive cell grid area.
39
+ * @csspart input-cell - Each cell's label element.
40
+ * @csspart control - The visible checkbox/radio box (tabular mode). Also carries the variant
41
+ * tokens `radio` | `checkbox`, `checked`, and `correct` | `incorrect` — e.g.
42
+ * `::part(control radio checked correct)`. These mirror the custom-state vocabulary; the cells
43
+ * are plain spans in the shadow root, so they cannot carry real `:state()`.
44
+ * @csspart control-mark - The inner mark element; carries the same variant tokens as `control`.
45
+ * @csspart message - Live validation message region (role="alert").
46
+ */
5
47
  export declare class QtiMatchInteraction extends QtiMatchInteraction_base {
6
- #private;
7
48
  static styles: CSSResultGroup;
49
+ /**
50
+ * Match targets are categories, not slots. They should look able to hold several answers, so they
51
+ * take a flat floor (the `--qti-dropzone-min-height` fallback) rather than the size of the widest chip.
52
+ *
53
+ * This is a property of the INTERACTION, not of an individual target. A `match-max="1"` target is
54
+ * still a category that happens to take one answer, and it sits in a grid beside targets that take
55
+ * several — sizing each one to its own contents would make that grid ragged. So the whole
56
+ * interaction opts out, and the per-target `match-max` decides nothing about size here.
57
+ *
58
+ * The interactions where a drop IS a slot — gap-match, order, associate — keep measurement on, and
59
+ * their drops are exactly the widest chip plus the inset.
60
+ */
61
+ autoSizeDropzones: boolean;
8
62
  protected sourceChoices: QtiSimpleAssociableChoice[];
9
63
  protected targetChoices: QtiSimpleAssociableChoice[];
10
64
  protected lastCheckedRadio: HTMLInputElement | null;
@@ -12,12 +66,13 @@ export declare class QtiMatchInteraction extends QtiMatchInteraction_base {
12
66
  protected _response: string | string[];
13
67
  get response(): string[];
14
68
  set response(val: string[]);
69
+ isDragDropEnabled(): boolean;
70
+ initiateDrag(...args: any[]): void;
71
+ afterCache(): void;
15
72
  responseIdentifier: string;
16
- protected correctOptions: {
17
- source: string;
18
- target: string;
19
- }[];
20
73
  connectedCallback(): Promise<void>;
74
+ protected willUpdate(changed: PropertyValues): void;
75
+ private syncTabularSlotting;
21
76
  protected handleRadioClick: (e: {
22
77
  target: HTMLInputElement;
23
78
  }) => void;
@@ -25,8 +80,8 @@ export declare class QtiMatchInteraction extends QtiMatchInteraction_base {
25
80
  target: any;
26
81
  }) => void;
27
82
  validate(): boolean;
28
- toggleInternalCorrectResponse(show: boolean): void;
29
- toggleCandidateCorrection(show: boolean): void;
83
+ /** Extension hook for packages that add visual state tokens to tabular cells. */
84
+ protected getCellStateVariant(_value: string, _checked: boolean): string;
30
85
  render(): import("lit").TemplateResult<1>;
31
86
  }
32
87
  declare global {
@@ -6,20 +6,73 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { html, nothing } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
- import { unsafeHTML } from 'lit/directives/unsafe-html.js';
10
9
  import { Interaction } from '@qti-components/base';
11
10
  import { DragDropSlottedMixin, DragDropSlottedSortableMixin } from '@qti-components/interactions-core/mixins/drag-drop-observables';
12
11
  // import { DragDropInteractionMixin } from '@qti-components/interactions-core/mixins/drag-drop';
13
12
  import styles from './qti-match-interaction.styles';
14
- const SlottedBase = DragDropSlottedMixin(Interaction, 'qti-simple-match-set:first-of-type qti-simple-associable-choice, qti-simple-match-set:last-of-type > qti-simple-associable-choice > qti-simple-associable-choice', 'qti-simple-match-set:last-of-type > qti-simple-associable-choice', 'qti-simple-match-set:first-of-type');
15
- export class QtiMatchInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable="true"]') {
13
+ import tabularStyles from './qti-match-interaction-tabular.styles';
14
+ /**
15
+ * Match interaction: candidates pair items from two sets, either as drag-and-drop or as a tabular grid.
16
+ *
17
+ * @customElement qti-match-interaction
18
+ *
19
+ * @attr {string} response-identifier - Required. Identifier of the bound response variable.
20
+ * @attr {number} [max-associations=0] - Maximum pairings across the whole interaction; `0`
21
+ * means unlimited. QTI's own default is `1`; this element defaults to unlimited because a
22
+ * literal `1` breaks any multi-pair match item. See plans/interaction-attribute-spec-conformance.md.
23
+ * @attr {number} [min-associations=1] - Minimum pairings for a valid response.
24
+ * @attr {boolean} [shuffle=false] - Requests shuffling of both match sets. Applied by the
25
+ * transform pipeline (`qti-transformers`), not by this element.
26
+ * @attr {'qti-match-tabular'|'qti-choices-top'|'qti-choices-bottom'|'qti-choices-left'|'qti-choices-right'} class -
27
+ * QTI shared presentation vocabulary. `qti-match-tabular` renders a checkbox/radio grid
28
+ * instead of drag-and-drop; the `qti-choices-*` values position the source set.
29
+ * @attr {boolean} [auto-size-dropzones=false] - Extension, not QTI. Sizes every drop target to
30
+ * the widest chip so placement does not reflow the layout. The mixin default is `true`; this
31
+ * interaction is the one that overrides it to `false`, because a match target is a category card
32
+ * rather than a socket (see the field below and DROP-SIZING.md §1).
33
+ * @attr {boolean} [disable-animations=false] - Extension, not QTI. Disables the FLIP move
34
+ * animation.
35
+ *
36
+ * @slot prompt - The prompt shown above the interaction.
37
+ * @slot match-cols - Column headers (tabular mode).
38
+ * @slot match-rows - Row headers (tabular mode).
39
+ * @slot - Default slot for the two match sets (drag-and-drop mode).
40
+ *
41
+ * @csspart grid - The tabular grid container.
42
+ * @csspart corner - The top-left corner cell of the grid.
43
+ * @csspart cols-wrap - Wrapper row for the column headers.
44
+ * @csspart c-header - The column header slot wrapper.
45
+ * @csspart rows-wrap - Wrapper column for the row headers.
46
+ * @csspart r-header - The row header slot wrapper.
47
+ * @csspart checkbox-grid - The interactive cell grid area.
48
+ * @csspart input-cell - Each cell's label element.
49
+ * @csspart control - The visible checkbox/radio box (tabular mode). Also carries the variant
50
+ * tokens `radio` | `checkbox`, `checked`, and `correct` | `incorrect` — e.g.
51
+ * `::part(control radio checked correct)`. These mirror the custom-state vocabulary; the cells
52
+ * are plain spans in the shadow root, so they cannot carry real `:state()`.
53
+ * @csspart control-mark - The inner mark element; carries the same variant tokens as `control`.
54
+ * @csspart message - Live validation message region (role="alert").
55
+ */
56
+ export class QtiMatchInteraction extends DragDropSlottedSortableMixin(DragDropSlottedMixin(Interaction, 'qti-simple-match-set:first-of-type qti-simple-associable-choice, qti-simple-match-set:last-of-type > qti-simple-associable-choice > qti-simple-associable-choice', 'qti-simple-match-set:last-of-type > qti-simple-associable-choice', 'qti-simple-match-set:first-of-type'), '[qti-draggable="true"]') {
16
57
  constructor() {
17
58
  super(...arguments);
59
+ /**
60
+ * Match targets are categories, not slots. They should look able to hold several answers, so they
61
+ * take a flat floor (the `--qti-dropzone-min-height` fallback) rather than the size of the widest chip.
62
+ *
63
+ * This is a property of the INTERACTION, not of an individual target. A `match-max="1"` target is
64
+ * still a category that happens to take one answer, and it sits in a grid beside targets that take
65
+ * several — sizing each one to its own contents would make that grid ragged. So the whole
66
+ * interaction opts out, and the per-target `match-max` decides nothing about size here.
67
+ *
68
+ * The interactions where a drop IS a slot — gap-match, order, associate — keep measurement on, and
69
+ * their drops are exactly the widest chip plus the inset.
70
+ */
71
+ this.autoSizeDropzones = false;
18
72
  this.lastCheckedRadio = null;
19
73
  this.class = '';
20
74
  this._response = [];
21
75
  this.responseIdentifier = '';
22
- this.correctOptions = null;
23
76
  this.handleRadioClick = (e) => {
24
77
  const radio = e.target;
25
78
  if (this.lastCheckedRadio === radio) {
@@ -63,7 +116,7 @@ export class QtiMatchInteraction extends DragDropSlottedSortableMixin(SlottedBas
63
116
  }));
64
117
  };
65
118
  }
66
- static { this.styles = styles; }
119
+ static { this.styles = [styles, tabularStyles]; }
67
120
  // dragDropApi: TouchDragAndDrop;
68
121
  get response() {
69
122
  if (!this.classList.contains('qti-match-tabular'))
@@ -77,172 +130,153 @@ export class QtiMatchInteraction extends DragDropSlottedSortableMixin(SlottedBas
77
130
  else
78
131
  this._response = val;
79
132
  }
133
+ // Tabular mode is a pure radio/checkbox grid — drag-drop has no role there, so the choices
134
+ // must not advertise themselves as draggable either. Suppresses `:state(drag)` on them.
135
+ isDragDropEnabled() {
136
+ return !this.classList.contains('qti-match-tabular');
137
+ }
138
+ // Tabular mode is a pure radio/checkbox grid — drag-drop has no role there.
139
+ // Short-circuit drag initiation so no clone is created and no shadow/lightdom
140
+ // state can diverge from this._response.
141
+ initiateDrag(...args) {
142
+ if (this.classList.contains('qti-match-tabular'))
143
+ return;
144
+ super.initiateDrag(...args);
145
+ }
146
+ // Skip the drag-drop auto-sizing pass in tabular mode — it would write
147
+ // inline min-width/min-height onto each choice that forces the grid columns
148
+ // to the draggable's natural width.
149
+ afterCache() {
150
+ if (this.classList.contains('qti-match-tabular'))
151
+ return;
152
+ super.afterCache();
153
+ }
80
154
  async connectedCallback() {
81
155
  super.connectedCallback();
82
156
  this.sourceChoices = Array.from(this.querySelectorAll('qti-simple-match-set:first-of-type qti-simple-associable-choice'));
83
157
  this.targetChoices = Array.from(this.querySelectorAll('qti-simple-match-set:last-of-type qti-simple-associable-choice'));
84
- this.response = [];
158
+ this.syncTabularSlotting();
85
159
  }
86
- validate() {
87
- if (this.class.split(' ').includes('qti-match-tabular')) {
88
- return this.response?.length === this.sourceChoices.length;
89
- }
90
- else {
91
- return super.validate();
92
- }
160
+ willUpdate(changed) {
161
+ super.willUpdate?.(changed);
162
+ if (changed.has('class'))
163
+ this.syncTabularSlotting();
93
164
  }
94
- #getMatches(responseVariable) {
95
- if (!responseVariable.correctResponse) {
96
- return [];
97
- }
98
- const correctResponse = Array.isArray(responseVariable.correctResponse)
99
- ? responseVariable.correctResponse
100
- : [responseVariable.correctResponse];
101
- const matches = [];
102
- if (correctResponse) {
103
- correctResponse.forEach(x => {
104
- const split = x.split(' ');
105
- matches.push({ source: split[0], target: split[1] });
165
+ syncTabularSlotting() {
166
+ const isTabular = this.classList.contains('qti-match-tabular');
167
+ const sets = this.querySelectorAll(':scope > qti-simple-match-set');
168
+ const sourceSet = sets[0];
169
+ const targetSet = sets[1];
170
+ const stripSizing = (c) => {
171
+ c.style.removeProperty('min-width');
172
+ c.style.removeProperty('min-height');
173
+ c.style.removeProperty('--qti-dropzone-min-height');
174
+ };
175
+ if (isTabular) {
176
+ sourceSet?.setAttribute('slot', 'match-rows');
177
+ targetSet?.setAttribute('slot', 'match-cols');
178
+ // Choices flow into the subgrid wrappers via DOM order — no per-choice
179
+ // grid-row/grid-column writes required. --qti-match-rows/-cols are set
180
+ // inline on [part='grid'] from render() so the editor (which can't write
181
+ // to host style under ProseMirror) shares the same mechanism.
182
+ this.sourceChoices?.forEach(c => {
183
+ stripSizing(c);
184
+ c.setAttribute('role', 'rowheader');
185
+ });
186
+ this.targetChoices?.forEach(c => {
187
+ stripSizing(c);
188
+ c.setAttribute('role', 'columnheader');
106
189
  });
107
190
  }
108
- return matches;
109
- }
110
- toggleInternalCorrectResponse(show) {
111
- const responseVariable = this.responseVariable;
112
- if (!responseVariable?.correctResponse) {
113
- // Remove all previously added correct responses
114
- this.querySelectorAll('.correct-option').forEach(el => el.remove());
115
- return;
191
+ else {
192
+ sourceSet?.removeAttribute('slot');
193
+ targetSet?.removeAttribute('slot');
194
+ this.sourceChoices?.forEach(c => c.removeAttribute('role'));
195
+ this.targetChoices?.forEach(c => c.removeAttribute('role'));
116
196
  }
117
- const matches = this.#getMatches(responseVariable);
118
- if (!this.class.split(' ').includes('qti-match-tabular')) {
119
- if (show) {
120
- // Clear old correct options first
121
- this.querySelectorAll('.correct-option').forEach(el => el.remove());
122
- this.targetChoices.forEach(targetChoice => {
123
- const targetId = targetChoice.getAttribute('identifier');
124
- const match = matches.find(m => m.target === targetId);
125
- if (match?.source) {
126
- const sourceChoice = this.querySelector(`qti-simple-associable-choice[identifier="${match.source}"]`);
127
- const text = sourceChoice?.textContent?.trim();
128
- if (text && !targetChoice.previousElementSibling?.classList.contains('correct-option')) {
129
- const textSpan = document.createElement('span');
130
- textSpan.classList.add('correct-option');
131
- textSpan.textContent = text;
132
- // Style the span
133
- textSpan.style.border = '1px solid var(--qti-correct)';
134
- textSpan.style.borderRadius = '4px';
135
- textSpan.style.padding = '2px 4px';
136
- textSpan.style.display = 'inline-block';
137
- // Insert before the target choice
138
- targetChoice.insertAdjacentElement('beforebegin', textSpan);
139
- }
140
- }
141
- });
142
- }
143
- else {
144
- this.correctOptions = null;
145
- }
197
+ }
198
+ validate() {
199
+ if (this.class.split(' ').includes('qti-match-tabular')) {
200
+ return this.response?.length === this.sourceChoices.length;
146
201
  }
147
202
  else {
148
- if (show) {
149
- this.correctOptions = matches || [];
150
- }
151
- else {
152
- this.correctOptions = null;
153
- }
203
+ return super.validate();
154
204
  }
155
205
  }
156
- toggleCandidateCorrection(show) {
157
- const responseVariable = this.responseVariable;
158
- if (!responseVariable?.correctResponse) {
159
- return;
160
- }
161
- const matches = this.#getMatches(responseVariable);
162
- this.targetChoices.forEach(targetChoice => {
163
- const targetId = targetChoice.getAttribute('identifier');
164
- const targetMatches = matches.filter(m => m.target === targetId);
165
- const selectedChoices = targetChoice.querySelectorAll(`qti-simple-associable-choice`);
166
- selectedChoices.forEach(selectedChoice => {
167
- selectedChoice.internals.states.delete('candidate-correct');
168
- selectedChoice.internals.states.delete('candidate-incorrect');
169
- if (!show) {
170
- return;
171
- }
172
- const isCorrect = targetMatches.find(m => m.source === selectedChoice.identifier)?.source !== undefined;
173
- if (isCorrect) {
174
- selectedChoice.internals.states.add('candidate-correct');
175
- }
176
- else {
177
- selectedChoice.internals.states.add('candidate-incorrect');
178
- }
179
- });
180
- });
206
+ /** Extension hook for packages that add visual state tokens to tabular cells. */
207
+ getCellStateVariant(_value, _checked) {
208
+ return '';
181
209
  }
182
210
  render() {
183
211
  const isTabular = this.class.split(' ').includes('qti-match-tabular');
184
- const hasCorrectResponse = this.correctOptions !== null;
185
212
  return html `
186
213
  <slot name="prompt"></slot>
187
214
  <slot ?hidden=${isTabular}></slot>
188
215
 
189
216
  ${isTabular
190
217
  ? html `
191
- <table part="table">
192
- <tr part="r-header">
193
- <td></td>
194
- ${this.targetChoices.map(col => html `<th part="r-header">${unsafeHTML(col.innerHTML)}</th>`)}
195
- </tr>
196
-
197
- ${this.sourceChoices.map(row => html `<tr part="row">
198
- <td part="c-header">${unsafeHTML(row.innerHTML)}</td>
199
- ${this.targetChoices.map(col => {
218
+ <div
219
+ part="grid"
220
+ role="grid"
221
+ style="--qti-match-rows: ${this.sourceChoices.length}; --qti-match-cols: ${this.targetChoices.length};"
222
+ >
223
+ <div part="corner"></div>
224
+ <div part="cols-wrap" role="row"><slot name="match-cols" part="c-header"></slot></div>
225
+ <div part="rows-wrap"><slot name="match-rows" part="r-header"></slot></div>
226
+ <div part="checkbox-grid">
227
+ ${this.sourceChoices.map((row, r) => html `
228
+ <div role="row" style="display: contents;">
229
+ ${this.targetChoices.map((col, c) => {
200
230
  const rowId = row.getAttribute('identifier');
201
231
  const colId = col.getAttribute('identifier');
202
232
  const value = `${rowId} ${colId}`;
203
233
  const selectedInRowCount = (this.response || []).filter(v => v.split(' ')[0] === rowId).length || 0;
204
234
  const checked = this.response?.includes(value) || false;
205
235
  const type = row.matchMax === 1 ? 'radio' : 'checkbox';
206
- const isCorrect = !!this.correctOptions?.find(option => option.source === rowId && option.target === colId);
207
- const part = type === 'radio'
208
- ? `rb ${checked ? 'rb-checked' : ''} ${hasCorrectResponse ? (isCorrect ? 'rb-correct' : 'rb-incorrect') : ''}`
209
- : `cb ${checked ? 'cb-checked' : ''} ${hasCorrectResponse ? (isCorrect ? 'cb-correct' : 'cb-incorrect') : ''}`;
210
- // disable if match max is greater than 1 and max is reached
211
- const disable = this.correctOptions?.length > 0
212
- ? true
213
- : row.matchMax === 1
214
- ? false
215
- : row.matchMax !== 0 && selectedInRowCount >= row.matchMax && !checked;
216
- return html `<td part="input-cell">
217
- <div
218
- class="input-container"
219
- style="position: relative; width: 24px; height: 24px; margin: 0 auto;"
220
- >
221
- <input
222
- type=${type}
223
- part=${part}
224
- name=${rowId}
225
- value=${value}
226
- .disabled=${disable}
227
- @change=${(e) => this.handleRadioChange(e)}
228
- @click=${(e) => row.matchMax === 1 ? this.handleRadioClick(e) : null}
229
- />
230
- ${type === 'checkbox' && checked
231
- ? html `
232
- <svg
233
- part="checkmark"
234
- viewBox="0 0 24 24"
235
- style="position: absolute; width: 20px; height: 20px; top: 2px; left: 2px; pointer-events: none;"
236
- >
237
- <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" fill="white" />
238
- </svg>
239
- `
240
- : ''}
241
- </div>
242
- </td>`;
236
+ /*
237
+ * The cells are plain spans in this interaction's shadow root, so they
238
+ * have no ElementInternals and cannot carry `:state()` — and
239
+ * `::part(x):state(y)` requires the state to live on the part's own
240
+ * element. Multi-token parts are the correct mechanism here. The tokens
241
+ * deliberately reuse the state vocabulary (`radio`, `checkbox`,
242
+ * `checked`, `correct`, `incorrect`) so a themer reads the same words
243
+ * everywhere.
244
+ *
245
+ */
246
+ const stateVariant = this.getCellStateVariant(value, checked);
247
+ const checkedMarker = checked ? 'checked' : '';
248
+ const controlPart = `control ${type} ${checkedMarker} ${stateVariant}`.trim();
249
+ const controlMarkPart = `control-mark ${type} ${checkedMarker} ${stateVariant}`.trim();
250
+ const disable = row.matchMax === 1
251
+ ? false
252
+ : row.matchMax !== 0 && selectedInRowCount >= row.matchMax && !checked;
253
+ return html `
254
+ <label
255
+ part="input-cell"
256
+ role="gridcell"
257
+ style="grid-row: ${r + 1}; grid-column: ${c +
258
+ 1}; display: flex; align-items: center; justify-content: center; cursor: pointer;"
259
+ >
260
+ <input
261
+ type=${type}
262
+ hidden
263
+ name=${rowId}
264
+ value=${value}
265
+ .checked=${checked}
266
+ .disabled=${disable}
267
+ @change=${(e) => this.handleRadioChange(e)}
268
+ @click=${(e) => row.matchMax === 1 ? this.handleRadioClick(e) : null}
269
+ />
270
+ <span part=${controlPart} role=${type} aria-checked=${checked ? 'true' : 'false'}>
271
+ <span part=${controlMarkPart}></span>
272
+ </span>
273
+ </label>
274
+ `;
243
275
  })}
244
- </tr>`)}
245
- </table>
276
+ </div>
277
+ `)}
278
+ </div>
279
+ </div>
246
280
  `
247
281
  : nothing}
248
282
 
@@ -259,7 +293,4 @@ __decorate([
259
293
  __decorate([
260
294
  property({ type: String, attribute: 'response-identifier' })
261
295
  ], QtiMatchInteraction.prototype, "responseIdentifier", void 0);
262
- __decorate([
263
- state()
264
- ], QtiMatchInteraction.prototype, "correctOptions", void 0);
265
296
  //# sourceMappingURL=qti-match-interaction.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qti-match-interaction.js","sourceRoot":"","sources":["../src/qti-match-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, nothing } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { unsafeHTML } from 'lit/directives/unsafe-html.js';\n\nimport { Interaction } from '@qti-components/base';\nimport {\n DragDropSlottedMixin,\n DragDropSlottedSortableMixin\n} from '@qti-components/interactions-core/mixins/drag-drop-observables';\n\n// import { DragDropInteractionMixin } from '@qti-components/interactions-core/mixins/drag-drop';\nimport styles from './qti-match-interaction.styles';\n\nimport type { ResponseVariable } from '@qti-components/base';\nimport type { CSSResultGroup } from 'lit';\nimport type { ResponseInteraction } from '@qti-components/base';\nimport type { QtiSimpleAssociableChoice } from '@qti-components/interactions-core/elements/qti-simple-associable-choice';\nconst SlottedBase = DragDropSlottedMixin(\n Interaction,\n 'qti-simple-match-set:first-of-type qti-simple-associable-choice, qti-simple-match-set:last-of-type > qti-simple-associable-choice > qti-simple-associable-choice',\n 'qti-simple-match-set:last-of-type > qti-simple-associable-choice',\n 'qti-simple-match-set:first-of-type'\n);\n\nexport class QtiMatchInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable=\"true\"]') {\n static override styles: CSSResultGroup = styles;\n\n protected sourceChoices: QtiSimpleAssociableChoice[];\n protected targetChoices: QtiSimpleAssociableChoice[];\n protected lastCheckedRadio: HTMLInputElement | null = null;\n\n @property({ type: String }) class: string = '';\n\n @state() protected _response: string | string[] = [];\n // dragDropApi: TouchDragAndDrop;\n get response(): string[] {\n if (!this.classList.contains('qti-match-tabular')) return super.response as string[];\n else return this._response as string[];\n }\n set response(val: string[]) {\n if (!this.classList.contains('qti-match-tabular')) super.response = val;\n else this._response = val;\n }\n\n @property({ type: String, attribute: 'response-identifier' }) override responseIdentifier: string = '';\n\n @state() protected correctOptions: { source: string; target: string }[] = null;\n\n override async connectedCallback(): Promise<void> {\n super.connectedCallback();\n this.sourceChoices = Array.from<QtiSimpleAssociableChoice>(\n this.querySelectorAll('qti-simple-match-set:first-of-type qti-simple-associable-choice')\n );\n this.targetChoices = Array.from<QtiSimpleAssociableChoice>(\n this.querySelectorAll('qti-simple-match-set:last-of-type qti-simple-associable-choice')\n );\n\n this.response = [];\n }\n\n protected handleRadioClick = (e: { target: HTMLInputElement }) => {\n const radio = e.target as HTMLInputElement;\n if (this.lastCheckedRadio === radio) {\n radio.checked = false;\n this.lastCheckedRadio = null;\n this.handleRadioChange(e);\n } else {\n this.lastCheckedRadio = radio;\n }\n };\n\n protected handleRadioChange = (e: { target: any }) => {\n const checkbox = e.target as HTMLInputElement;\n const value = checkbox.value;\n const name = checkbox.name;\n const type = checkbox.type;\n\n if (checkbox.checked) {\n if (!this.response) {\n this.response = [value];\n } else if (this.response.indexOf(value) === -1) {\n if (type === 'radio') {\n this.response = (this.response || []).filter(v => v.indexOf(name) === -1);\n }\n this.response = [...this.response, value];\n }\n this.lastCheckedRadio = checkbox;\n } else {\n this.response = (this.response || []).filter(v => v !== value);\n this.lastCheckedRadio = null;\n }\n\n this.requestUpdate();\n this.dispatchEvent(\n new CustomEvent<ResponseInteraction>('qti-interaction-response', {\n bubbles: true,\n composed: true,\n detail: {\n responseIdentifier: this.responseIdentifier,\n response: Array.isArray(this.response) ? [...this.response] : this.response\n }\n })\n );\n };\n\n validate(): boolean {\n if (this.class.split(' ').includes('qti-match-tabular')) {\n return this.response?.length === this.sourceChoices.length;\n } else {\n return super.validate();\n }\n }\n\n #getMatches(responseVariable: ResponseVariable): { source: string; target: string }[] {\n if (!responseVariable.correctResponse) {\n return [];\n }\n const correctResponse = Array.isArray(responseVariable.correctResponse)\n ? responseVariable.correctResponse\n : [responseVariable.correctResponse];\n\n const matches: { source: string; target: string }[] = [];\n if (correctResponse) {\n correctResponse.forEach(x => {\n const split = x.split(' ');\n matches.push({ source: split[0], target: split[1] });\n });\n }\n return matches;\n }\n\n public override toggleInternalCorrectResponse(show: boolean): void {\n const responseVariable = this.responseVariable;\n\n if (!responseVariable?.correctResponse) {\n // Remove all previously added correct responses\n this.querySelectorAll('.correct-option').forEach(el => el.remove());\n return;\n }\n const matches = this.#getMatches(responseVariable);\n\n if (!this.class.split(' ').includes('qti-match-tabular')) {\n if (show) {\n // Clear old correct options first\n this.querySelectorAll('.correct-option').forEach(el => el.remove());\n\n this.targetChoices.forEach(targetChoice => {\n const targetId = targetChoice.getAttribute('identifier');\n const match = matches.find(m => m.target === targetId);\n\n if (match?.source) {\n const sourceChoice = this.querySelector(`qti-simple-associable-choice[identifier=\"${match.source}\"]`);\n const text = sourceChoice?.textContent?.trim();\n\n if (text && !targetChoice.previousElementSibling?.classList.contains('correct-option')) {\n const textSpan = document.createElement('span');\n textSpan.classList.add('correct-option');\n textSpan.textContent = text;\n\n // Style the span\n textSpan.style.border = '1px solid var(--qti-correct)';\n textSpan.style.borderRadius = '4px';\n textSpan.style.padding = '2px 4px';\n textSpan.style.display = 'inline-block';\n\n // Insert before the target choice\n targetChoice.insertAdjacentElement('beforebegin', textSpan);\n }\n }\n });\n } else {\n this.correctOptions = null;\n }\n } else {\n if (show) {\n this.correctOptions = matches || [];\n } else {\n this.correctOptions = null;\n }\n }\n }\n\n public override toggleCandidateCorrection(show: boolean) {\n const responseVariable = this.responseVariable;\n\n if (!responseVariable?.correctResponse) {\n return;\n }\n const matches = this.#getMatches(responseVariable);\n\n this.targetChoices.forEach(targetChoice => {\n const targetId = targetChoice.getAttribute('identifier');\n const targetMatches = matches.filter(m => m.target === targetId);\n\n const selectedChoices = targetChoice.querySelectorAll(`qti-simple-associable-choice`);\n\n selectedChoices.forEach(selectedChoice => {\n selectedChoice.internals.states.delete('candidate-correct');\n selectedChoice.internals.states.delete('candidate-incorrect');\n\n if (!show) {\n return;\n }\n\n const isCorrect = targetMatches.find(m => m.source === selectedChoice.identifier)?.source !== undefined;\n if (isCorrect) {\n selectedChoice.internals.states.add('candidate-correct');\n } else {\n selectedChoice.internals.states.add('candidate-incorrect');\n }\n });\n });\n }\n\n override render() {\n const isTabular = this.class.split(' ').includes('qti-match-tabular');\n const hasCorrectResponse = this.correctOptions !== null;\n return html`\n <slot name=\"prompt\"></slot>\n <slot ?hidden=${isTabular}></slot>\n\n ${isTabular\n ? html`\n <table part=\"table\">\n <tr part=\"r-header\">\n <td></td>\n ${this.targetChoices.map(col => html`<th part=\"r-header\">${unsafeHTML(col.innerHTML)}</th>`)}\n </tr>\n\n ${this.sourceChoices.map(\n row =>\n html`<tr part=\"row\">\n <td part=\"c-header\">${unsafeHTML(row.innerHTML)}</td>\n ${this.targetChoices.map(col => {\n const rowId = row.getAttribute('identifier');\n const colId = col.getAttribute('identifier');\n const value = `${rowId} ${colId}`;\n const selectedInRowCount =\n (this.response || []).filter(v => v.split(' ')[0] === rowId).length || 0;\n const checked = this.response?.includes(value) || false;\n const type = row.matchMax === 1 ? 'radio' : 'checkbox';\n const isCorrect = !!this.correctOptions?.find(\n option => option.source === rowId && option.target === colId\n );\n const part =\n type === 'radio'\n ? `rb ${checked ? 'rb-checked' : ''} ${hasCorrectResponse ? (isCorrect ? 'rb-correct' : 'rb-incorrect') : ''}`\n : `cb ${checked ? 'cb-checked' : ''} ${hasCorrectResponse ? (isCorrect ? 'cb-correct' : 'cb-incorrect') : ''}`;\n\n // disable if match max is greater than 1 and max is reached\n const disable =\n this.correctOptions?.length > 0\n ? true\n : row.matchMax === 1\n ? false\n : row.matchMax !== 0 && selectedInRowCount >= row.matchMax && !checked;\n return html`<td part=\"input-cell\">\n <div\n class=\"input-container\"\n style=\"position: relative; width: 24px; height: 24px; margin: 0 auto;\"\n >\n <input\n type=${type}\n part=${part}\n name=${rowId}\n value=${value}\n .disabled=${disable}\n @change=${(e: { target: any }) => this.handleRadioChange(e)}\n @click=${(e: { target: HTMLInputElement }) =>\n row.matchMax === 1 ? this.handleRadioClick(e) : null}\n />\n ${type === 'checkbox' && checked\n ? html`\n <svg\n part=\"checkmark\"\n viewBox=\"0 0 24 24\"\n style=\"position: absolute; width: 20px; height: 20px; top: 2px; left: 2px; pointer-events: none;\"\n >\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"white\" />\n </svg>\n `\n : ''}\n </div>\n </td>`;\n })}\n </tr>`\n )}\n </table>\n `\n : nothing}\n\n <div role=\"alert\" part=\"message\" id=\"validation-message\"></div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-match-interaction': QtiMatchInteraction;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-match-interaction.js","sourceRoot":"","sources":["../src/qti-match-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, nothing } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { Interaction } from '@qti-components/base';\nimport {\n DragDropSlottedMixin,\n DragDropSlottedSortableMixin\n} from '@qti-components/interactions-core/mixins/drag-drop-observables';\n\n// import { DragDropInteractionMixin } from '@qti-components/interactions-core/mixins/drag-drop';\nimport styles from './qti-match-interaction.styles';\nimport tabularStyles from './qti-match-interaction-tabular.styles';\n\nimport type { ResponseVariable } from '@qti-components/base';\nimport type { CSSResultGroup, PropertyValues } from 'lit';\nimport type { ResponseInteraction } from '@qti-components/base';\nimport type { QtiSimpleAssociableChoice } from '@qti-components/interactions-core/elements/qti-simple-associable-choice';\n/**\n * Match interaction: candidates pair items from two sets, either as drag-and-drop or as a tabular grid.\n *\n * @customElement qti-match-interaction\n *\n * @attr {string} response-identifier - Required. Identifier of the bound response variable.\n * @attr {number} [max-associations=0] - Maximum pairings across the whole interaction; `0`\n * means unlimited. QTI's own default is `1`; this element defaults to unlimited because a\n * literal `1` breaks any multi-pair match item. See plans/interaction-attribute-spec-conformance.md.\n * @attr {number} [min-associations=1] - Minimum pairings for a valid response.\n * @attr {boolean} [shuffle=false] - Requests shuffling of both match sets. Applied by the\n * transform pipeline (`qti-transformers`), not by this element.\n * @attr {'qti-match-tabular'|'qti-choices-top'|'qti-choices-bottom'|'qti-choices-left'|'qti-choices-right'} class -\n * QTI shared presentation vocabulary. `qti-match-tabular` renders a checkbox/radio grid\n * instead of drag-and-drop; the `qti-choices-*` values position the source set.\n * @attr {boolean} [auto-size-dropzones=false] - Extension, not QTI. Sizes every drop target to\n * the widest chip so placement does not reflow the layout. The mixin default is `true`; this\n * interaction is the one that overrides it to `false`, because a match target is a category card\n * rather than a socket (see the field below and DROP-SIZING.md §1).\n * @attr {boolean} [disable-animations=false] - Extension, not QTI. Disables the FLIP move\n * animation.\n *\n * @slot prompt - The prompt shown above the interaction.\n * @slot match-cols - Column headers (tabular mode).\n * @slot match-rows - Row headers (tabular mode).\n * @slot - Default slot for the two match sets (drag-and-drop mode).\n *\n * @csspart grid - The tabular grid container.\n * @csspart corner - The top-left corner cell of the grid.\n * @csspart cols-wrap - Wrapper row for the column headers.\n * @csspart c-header - The column header slot wrapper.\n * @csspart rows-wrap - Wrapper column for the row headers.\n * @csspart r-header - The row header slot wrapper.\n * @csspart checkbox-grid - The interactive cell grid area.\n * @csspart input-cell - Each cell's label element.\n * @csspart control - The visible checkbox/radio box (tabular mode). Also carries the variant\n * tokens `radio` | `checkbox`, `checked`, and `correct` | `incorrect` — e.g.\n * `::part(control radio checked correct)`. These mirror the custom-state vocabulary; the cells\n * are plain spans in the shadow root, so they cannot carry real `:state()`.\n * @csspart control-mark - The inner mark element; carries the same variant tokens as `control`.\n * @csspart message - Live validation message region (role=\"alert\").\n */\nexport class QtiMatchInteraction extends DragDropSlottedSortableMixin(\n DragDropSlottedMixin(\n Interaction,\n 'qti-simple-match-set:first-of-type qti-simple-associable-choice, qti-simple-match-set:last-of-type > qti-simple-associable-choice > qti-simple-associable-choice',\n 'qti-simple-match-set:last-of-type > qti-simple-associable-choice',\n 'qti-simple-match-set:first-of-type'\n ),\n '[qti-draggable=\"true\"]'\n) {\n static override styles: CSSResultGroup = [styles, tabularStyles];\n\n /**\n * Match targets are categories, not slots. They should look able to hold several answers, so they\n * take a flat floor (the `--qti-dropzone-min-height` fallback) rather than the size of the widest chip.\n *\n * This is a property of the INTERACTION, not of an individual target. A `match-max=\"1\"` target is\n * still a category that happens to take one answer, and it sits in a grid beside targets that take\n * several — sizing each one to its own contents would make that grid ragged. So the whole\n * interaction opts out, and the per-target `match-max` decides nothing about size here.\n *\n * The interactions where a drop IS a slot — gap-match, order, associate — keep measurement on, and\n * their drops are exactly the widest chip plus the inset.\n */\n public override autoSizeDropzones = false;\n\n protected sourceChoices: QtiSimpleAssociableChoice[];\n protected targetChoices: QtiSimpleAssociableChoice[];\n protected lastCheckedRadio: HTMLInputElement | null = null;\n\n @property({ type: String }) class: string = '';\n\n @state() protected _response: string | string[] = [];\n // dragDropApi: TouchDragAndDrop;\n get response(): string[] {\n if (!this.classList.contains('qti-match-tabular')) return super.response as string[];\n else return this._response as string[];\n }\n set response(val: string[]) {\n if (!this.classList.contains('qti-match-tabular')) super.response = val;\n else this._response = val;\n }\n\n // Tabular mode is a pure radio/checkbox grid — drag-drop has no role there, so the choices\n // must not advertise themselves as draggable either. Suppresses `:state(drag)` on them.\n public override isDragDropEnabled(): boolean {\n return !this.classList.contains('qti-match-tabular');\n }\n\n // Tabular mode is a pure radio/checkbox grid — drag-drop has no role there.\n // Short-circuit drag initiation so no clone is created and no shadow/lightdom\n // state can diverge from this._response.\n public override initiateDrag(...args: any[]): void {\n if (this.classList.contains('qti-match-tabular')) return;\n (super.initiateDrag as (...a: any[]) => void)(...args);\n }\n\n // Skip the drag-drop auto-sizing pass in tabular mode — it would write\n // inline min-width/min-height onto each choice that forces the grid columns\n // to the draggable's natural width.\n public override afterCache(): void {\n if (this.classList.contains('qti-match-tabular')) return;\n super.afterCache();\n }\n\n @property({ type: String, attribute: 'response-identifier' }) override responseIdentifier: string = '';\n\n override async connectedCallback(): Promise<void> {\n super.connectedCallback();\n this.sourceChoices = Array.from<QtiSimpleAssociableChoice>(\n this.querySelectorAll('qti-simple-match-set:first-of-type qti-simple-associable-choice')\n );\n this.targetChoices = Array.from<QtiSimpleAssociableChoice>(\n this.querySelectorAll('qti-simple-match-set:last-of-type qti-simple-associable-choice')\n );\n\n this.syncTabularSlotting();\n }\n\n protected override willUpdate(changed: PropertyValues): void {\n super.willUpdate?.(changed);\n if (changed.has('class')) this.syncTabularSlotting();\n }\n\n private syncTabularSlotting(): void {\n const isTabular = this.classList.contains('qti-match-tabular');\n const sets = this.querySelectorAll(':scope > qti-simple-match-set');\n const sourceSet = sets[0] as HTMLElement | undefined;\n const targetSet = sets[1] as HTMLElement | undefined;\n\n const stripSizing = (c: QtiSimpleAssociableChoice) => {\n c.style.removeProperty('min-width');\n c.style.removeProperty('min-height');\n c.style.removeProperty('--qti-dropzone-min-height');\n };\n if (isTabular) {\n sourceSet?.setAttribute('slot', 'match-rows');\n targetSet?.setAttribute('slot', 'match-cols');\n // Choices flow into the subgrid wrappers via DOM order — no per-choice\n // grid-row/grid-column writes required. --qti-match-rows/-cols are set\n // inline on [part='grid'] from render() so the editor (which can't write\n // to host style under ProseMirror) shares the same mechanism.\n this.sourceChoices?.forEach(c => {\n stripSizing(c);\n c.setAttribute('role', 'rowheader');\n });\n this.targetChoices?.forEach(c => {\n stripSizing(c);\n c.setAttribute('role', 'columnheader');\n });\n } else {\n sourceSet?.removeAttribute('slot');\n targetSet?.removeAttribute('slot');\n this.sourceChoices?.forEach(c => c.removeAttribute('role'));\n this.targetChoices?.forEach(c => c.removeAttribute('role'));\n }\n }\n\n protected handleRadioClick = (e: { target: HTMLInputElement }) => {\n const radio = e.target as HTMLInputElement;\n if (this.lastCheckedRadio === radio) {\n radio.checked = false;\n this.lastCheckedRadio = null;\n this.handleRadioChange(e);\n } else {\n this.lastCheckedRadio = radio;\n }\n };\n\n protected handleRadioChange = (e: { target: any }) => {\n const checkbox = e.target as HTMLInputElement;\n const value = checkbox.value;\n const name = checkbox.name;\n const type = checkbox.type;\n\n if (checkbox.checked) {\n if (!this.response) {\n this.response = [value];\n } else if (this.response.indexOf(value) === -1) {\n if (type === 'radio') {\n this.response = (this.response || []).filter(v => v.indexOf(name) === -1);\n }\n this.response = [...this.response, value];\n }\n this.lastCheckedRadio = checkbox;\n } else {\n this.response = (this.response || []).filter(v => v !== value);\n this.lastCheckedRadio = null;\n }\n\n this.requestUpdate();\n this.dispatchEvent(\n new CustomEvent<ResponseInteraction>('qti-interaction-response', {\n bubbles: true,\n composed: true,\n detail: {\n responseIdentifier: this.responseIdentifier,\n response: Array.isArray(this.response) ? [...this.response] : this.response\n }\n })\n );\n };\n\n validate(): boolean {\n if (this.class.split(' ').includes('qti-match-tabular')) {\n return this.response?.length === this.sourceChoices.length;\n } else {\n return super.validate();\n }\n }\n\n /** Extension hook for packages that add visual state tokens to tabular cells. */\n protected getCellStateVariant(_value: string, _checked: boolean): string {\n return '';\n }\n\n override render() {\n const isTabular = this.class.split(' ').includes('qti-match-tabular');\n return html`\n <slot name=\"prompt\"></slot>\n <slot ?hidden=${isTabular}></slot>\n\n ${isTabular\n ? html`\n <div\n part=\"grid\"\n role=\"grid\"\n style=\"--qti-match-rows: ${this.sourceChoices.length}; --qti-match-cols: ${this.targetChoices.length};\"\n >\n <div part=\"corner\"></div>\n <div part=\"cols-wrap\" role=\"row\"><slot name=\"match-cols\" part=\"c-header\"></slot></div>\n <div part=\"rows-wrap\"><slot name=\"match-rows\" part=\"r-header\"></slot></div>\n <div part=\"checkbox-grid\">\n ${this.sourceChoices.map(\n (row, r) => html`\n <div role=\"row\" style=\"display: contents;\">\n ${this.targetChoices.map((col, c) => {\n const rowId = row.getAttribute('identifier');\n const colId = col.getAttribute('identifier');\n const value = `${rowId} ${colId}`;\n const selectedInRowCount =\n (this.response || []).filter(v => v.split(' ')[0] === rowId).length || 0;\n const checked = this.response?.includes(value) || false;\n const type = row.matchMax === 1 ? 'radio' : 'checkbox';\n /*\n * The cells are plain spans in this interaction's shadow root, so they\n * have no ElementInternals and cannot carry `:state()` — and\n * `::part(x):state(y)` requires the state to live on the part's own\n * element. Multi-token parts are the correct mechanism here. The tokens\n * deliberately reuse the state vocabulary (`radio`, `checkbox`,\n * `checked`, `correct`, `incorrect`) so a themer reads the same words\n * everywhere.\n *\n */\n const stateVariant = this.getCellStateVariant(value, checked);\n const checkedMarker = checked ? 'checked' : '';\n const controlPart = `control ${type} ${checkedMarker} ${stateVariant}`.trim();\n const controlMarkPart = `control-mark ${type} ${checkedMarker} ${stateVariant}`.trim();\n const disable =\n row.matchMax === 1\n ? false\n : row.matchMax !== 0 && selectedInRowCount >= row.matchMax && !checked;\n return html`\n <label\n part=\"input-cell\"\n role=\"gridcell\"\n style=\"grid-row: ${r + 1}; grid-column: ${c +\n 1}; display: flex; align-items: center; justify-content: center; cursor: pointer;\"\n >\n <input\n type=${type}\n hidden\n name=${rowId}\n value=${value}\n .checked=${checked}\n .disabled=${disable}\n @change=${(e: { target: any }) => this.handleRadioChange(e)}\n @click=${(e: { target: HTMLInputElement }) =>\n row.matchMax === 1 ? this.handleRadioClick(e) : null}\n />\n <span part=${controlPart} role=${type} aria-checked=${checked ? 'true' : 'false'}>\n <span part=${controlMarkPart}></span>\n </span>\n </label>\n `;\n })}\n </div>\n `\n )}\n </div>\n </div>\n `\n : nothing}\n\n <div role=\"alert\" part=\"message\" id=\"validation-message\"></div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-match-interaction': QtiMatchInteraction;\n }\n}\n"]}
@@ -1,2 +1,2 @@
1
- declare const _default: import("lit").CSSResult;
1
+ declare const _default: import("lit").CSSResult[];
2
2
  export default _default;
@@ -1,31 +1,116 @@
1
1
  import { css } from 'lit';
2
+ import { boxSizing, validationMessage } from '@qti-components/base';
2
3
  // import componentStyles from '../../utilities/styles/component.styles';
3
4
  /* ${componentStyles} */
4
- export default css `
5
- slot:not([hidden]) {
6
- /* slot where the */
7
- display: flex;
8
- flex-direction: column;
9
- gap: 0.5rem;
10
- }
11
- :host(.qti-choices-top) slot {
12
- flex-direction: column;
13
- }
14
- :host(.qti-choices-bottom) slot {
15
- flex-direction: column-reverse;
16
- }
17
- :host(.qti-choices-left) slot {
18
- flex-direction: row;
19
- }
20
- :host(.qti-choices-right) slot {
21
- flex-direction: row-reverse;
22
- }
23
- slot[name='prompt'] {
24
- display: block;
25
- }
26
- ::slotted(qti-simple-match-set) {
27
- /* Make sure the drag and drop container slots have the same width */
28
- flex: 1;
29
- }
30
- `;
5
+ export default [
6
+ boxSizing,
7
+ validationMessage,
8
+ css `
9
+ /*
10
+ * A custom element is inline unless it says otherwise, and this one never did. Its shadow content
11
+ * is block-level, so block-in-inline generated the anonymous block boxes and it looked correct —
12
+ * but an inline box whose children are all block has no background area of its own, so the host
13
+ * could not be painted, outlined or given a radius by anything downstream. Declaring the box the
14
+ * element already had is layout-neutral: measured on ITEM009, the element rect and the following
15
+ * sibling's position are identical to the pixel with and without this.
16
+ */
17
+ :host {
18
+ display: block;
19
+
20
+ /*
21
+ * Match's columns are wider than the shared default, because each one is a category card
22
+ * rather than a slot holding one chip. This was the 150px fallback inside the grid rule
23
+ * below; a fallback could hold it only while nothing declared the token, so it is a real
24
+ * declaration now. On :host, so it survives with or without the theme — and so a theme can
25
+ * still retune it from the document, which outranks :host for the element it matches.
26
+ */
27
+ --qti-drop-track-min-width: 150px;
28
+ }
29
+
30
+ slot:not([hidden]) {
31
+ /* slot where the */
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 0.5rem;
35
+ }
36
+ :host(.qti-choices-top) slot {
37
+ flex-direction: column;
38
+ }
39
+ :host(.qti-choices-bottom) slot {
40
+ flex-direction: column-reverse;
41
+ }
42
+ :host(.qti-choices-left) slot {
43
+ flex-direction: row;
44
+ }
45
+ :host(.qti-choices-right) slot {
46
+ flex-direction: row-reverse;
47
+ }
48
+ slot[name='prompt'] {
49
+ display: block;
50
+ }
51
+ ::slotted(qti-simple-match-set) {
52
+ /* Make sure the drag and drop container slots have the same width */
53
+ flex: 1;
54
+ }
55
+
56
+ /*
57
+ * The two match sets, in drag-drop mode. Both are slotted, so ::slotted() reaches them; the
58
+ * theme used to lay them out by descendant selector from the document.
59
+ *
60
+ * First set is the chip bank — a wrapping row. Last set is the target grid, whose track floor is
61
+ * a token so a theme can decide how wide a category card gets. That floor used to be a bare
62
+ * '150px' in the theme, the one responsive breakpoint hiding in a stylesheet that is meant to
63
+ * hold none.
64
+ *
65
+ * The gaps stay in the theme: gap is spacing.
66
+ */
67
+ :host(:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:first-of-type) {
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+
71
+ /*
72
+ * Two different properties, and the bank needs both.
73
+ *
74
+ * align-items packs a chip to the top of ITS OWN line. align-content packs the LINES to the
75
+ * top of the container, and was missing — so it took the initial normal, which for a
76
+ * multi-line flex container behaves as stretch.
77
+ *
78
+ * That only shows once the bank wraps AND is taller than its content, which is exactly the
79
+ * side-by-side arrangement: the bank and the target grid are flex siblings, so the bank is
80
+ * stretched to the grid's height. Measured on ITEM008 at 740px, where the bank wraps to two
81
+ * lines: container 272px tall for 37px chips, the two lines stretched to 136px each, and the
82
+ * second row of chips floating 144px below the first instead of 53px.
83
+ */
84
+ align-items: flex-start;
85
+ align-content: flex-start;
86
+ }
87
+
88
+ :host(:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:last-of-type) {
89
+ display: grid;
90
+ /*
91
+ * The track floor by name — see the same rule in qti-order-interaction.styles.ts. Match sets
92
+ * it wider than the shared default below, because a category card has to look able to hold
93
+ * several answers. The measured term is still in the max() and still resolves to 0px here:
94
+ * this interaction sets autoSizeDropzones = false, so nothing ever publishes it. Written out
95
+ * anyway so the two grids read identically and neither has to be understood as the special one.
96
+ */
97
+ grid-template-columns: repeat(
98
+ auto-fit,
99
+ minmax(max(var(--qti-drop-track-min-width, 150px), var(--qti-dropzone-min-width, 0px)), 1fr)
100
+ );
101
+ }
102
+
103
+ /* An answer key shows the answer, not the bank you would have dragged it from. The attribute is
104
+ set on the clone by the correct-response mixin; the theme used to do this with a
105
+ '.full-correct-response' descendant selector, from the document. */
106
+ :host([answer-key]:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:first-of-type) {
107
+ display: none;
108
+ }
109
+
110
+ /* Same for a disabled interaction: nothing left to drag, so no bank. */
111
+ :host(:disabled:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:first-of-type) {
112
+ display: none;
113
+ }
114
+ `
115
+ ];
31
116
  //# sourceMappingURL=qti-match-interaction.styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qti-match-interaction.styles.js","sourceRoot":"","sources":["../src/qti-match-interaction.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC1B,yEAAyE;AAEzE,wBAAwB;AACxB,eAAe,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BjB,CAAC","sourcesContent":["import { css } from 'lit';\n// import componentStyles from '../../utilities/styles/component.styles';\n\n/* ${componentStyles} */\nexport default css`\n slot:not([hidden]) {\n /* slot where the */\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n }\n :host(.qti-choices-top) slot {\n flex-direction: column;\n }\n :host(.qti-choices-bottom) slot {\n flex-direction: column-reverse;\n }\n :host(.qti-choices-left) slot {\n flex-direction: row;\n }\n :host(.qti-choices-right) slot {\n flex-direction: row-reverse;\n }\n slot[name='prompt'] {\n display: block;\n }\n ::slotted(qti-simple-match-set) {\n /* Make sure the drag and drop container slots have the same width */\n flex: 1;\n }\n`;\n"]}
1
+ {"version":3,"file":"qti-match-interaction.styles.js","sourceRoot":"","sources":["../src/qti-match-interaction.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACpE,yEAAyE;AAEzE,wBAAwB;AACxB,eAAe;IACb,SAAS;IACT,iBAAiB;IACjB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GF;CACF,CAAC","sourcesContent":["import { css } from 'lit';\n\nimport { boxSizing, validationMessage } from '@qti-components/base';\n// import componentStyles from '../../utilities/styles/component.styles';\n\n/* ${componentStyles} */\nexport default [\n boxSizing,\n validationMessage,\n css`\n /*\n * A custom element is inline unless it says otherwise, and this one never did. Its shadow content\n * is block-level, so block-in-inline generated the anonymous block boxes and it looked correct —\n * but an inline box whose children are all block has no background area of its own, so the host\n * could not be painted, outlined or given a radius by anything downstream. Declaring the box the\n * element already had is layout-neutral: measured on ITEM009, the element rect and the following\n * sibling's position are identical to the pixel with and without this.\n */\n :host {\n display: block;\n\n /*\n * Match's columns are wider than the shared default, because each one is a category card\n * rather than a slot holding one chip. This was the 150px fallback inside the grid rule\n * below; a fallback could hold it only while nothing declared the token, so it is a real\n * declaration now. On :host, so it survives with or without the theme — and so a theme can\n * still retune it from the document, which outranks :host for the element it matches.\n */\n --qti-drop-track-min-width: 150px;\n }\n\n slot:not([hidden]) {\n /* slot where the */\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n }\n :host(.qti-choices-top) slot {\n flex-direction: column;\n }\n :host(.qti-choices-bottom) slot {\n flex-direction: column-reverse;\n }\n :host(.qti-choices-left) slot {\n flex-direction: row;\n }\n :host(.qti-choices-right) slot {\n flex-direction: row-reverse;\n }\n slot[name='prompt'] {\n display: block;\n }\n ::slotted(qti-simple-match-set) {\n /* Make sure the drag and drop container slots have the same width */\n flex: 1;\n }\n\n /*\n * The two match sets, in drag-drop mode. Both are slotted, so ::slotted() reaches them; the\n * theme used to lay them out by descendant selector from the document.\n *\n * First set is the chip bank — a wrapping row. Last set is the target grid, whose track floor is\n * a token so a theme can decide how wide a category card gets. That floor used to be a bare\n * '150px' in the theme, the one responsive breakpoint hiding in a stylesheet that is meant to\n * hold none.\n *\n * The gaps stay in the theme: gap is spacing.\n */\n :host(:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:first-of-type) {\n display: flex;\n flex-wrap: wrap;\n\n /*\n * Two different properties, and the bank needs both.\n *\n * align-items packs a chip to the top of ITS OWN line. align-content packs the LINES to the\n * top of the container, and was missing — so it took the initial normal, which for a\n * multi-line flex container behaves as stretch.\n *\n * That only shows once the bank wraps AND is taller than its content, which is exactly the\n * side-by-side arrangement: the bank and the target grid are flex siblings, so the bank is\n * stretched to the grid's height. Measured on ITEM008 at 740px, where the bank wraps to two\n * lines: container 272px tall for 37px chips, the two lines stretched to 136px each, and the\n * second row of chips floating 144px below the first instead of 53px.\n */\n align-items: flex-start;\n align-content: flex-start;\n }\n\n :host(:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:last-of-type) {\n display: grid;\n /*\n * The track floor by name — see the same rule in qti-order-interaction.styles.ts. Match sets\n * it wider than the shared default below, because a category card has to look able to hold\n * several answers. The measured term is still in the max() and still resolves to 0px here:\n * this interaction sets autoSizeDropzones = false, so nothing ever publishes it. Written out\n * anyway so the two grids read identically and neither has to be understood as the special one.\n */\n grid-template-columns: repeat(\n auto-fit,\n minmax(max(var(--qti-drop-track-min-width, 150px), var(--qti-dropzone-min-width, 0px)), 1fr)\n );\n }\n\n /* An answer key shows the answer, not the bank you would have dragged it from. The attribute is\n set on the clone by the correct-response mixin; the theme used to do this with a\n '.full-correct-response' descendant selector, from the document. */\n :host([answer-key]:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:first-of-type) {\n display: none;\n }\n\n /* Same for a disabled interaction: nothing left to drag, so no bank. */\n :host(:disabled:not(.qti-match-tabular)) ::slotted(qti-simple-match-set:first-of-type) {\n display: none;\n }\n `\n];\n"]}
package/dist/register.js CHANGED
@@ -1,3 +1,7 @@
1
- import { QtiMatchInteraction } from './qti-match-interaction';
2
- customElements.define('qti-match-interaction', QtiMatchInteraction);
1
+ import { elements } from './elements';
2
+ for (const { tag, ctor } of elements) {
3
+ if (!customElements.get(tag)) {
4
+ customElements.define(tag, ctor);
5
+ }
6
+ }
3
7
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,cAAc,CAAC,MAAM,CAAC,uBAAuB,EAAE,mBAAmB,CAAC,CAAC","sourcesContent":["import { QtiMatchInteraction } from './qti-match-interaction';\ncustomElements.define('qti-match-interaction', QtiMatchInteraction);\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;IACrC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;AACH,CAAC","sourcesContent":["import { elements } from './elements';\n\nfor (const { tag, ctor } of elements) {\n if (!customElements.get(tag)) {\n customElements.define(tag, ctor);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@qti-components/match-interaction",
3
3
  "description": "QTI match-interaction interaction",
4
- "version": "1.1.4",
4
+ "version": "2.0.0",
5
5
  "author": "",
6
6
  "dependencies": {
7
- "@qti-components/base": "^1.4.3",
8
- "@qti-components/interactions-core": "^1.1.4",
9
- "@qti-components/utilities": "^1.3.3"
7
+ "@qti-components/base": "^2.0.0",
8
+ "@qti-components/utilities": "^1.3.3",
9
+ "@qti-components/interactions-core": "^2.0.0"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@lit/context": "^1.1.6",
@@ -45,6 +45,10 @@
45
45
  "./styles": {
46
46
  "types": "./dist/qti-match-interaction.styles.d.ts",
47
47
  "import": "./dist/qti-match-interaction.styles.js"
48
+ },
49
+ "./elements": {
50
+ "types": "./dist/elements.d.ts",
51
+ "import": "./dist/elements.js"
48
52
  }
49
53
  },
50
54
  "repository": {
@@ -55,6 +59,8 @@
55
59
  "scripts": {
56
60
  "attw": "attw --profile esm-only --pack",
57
61
  "build": "tsc --paths null",
58
- "clean": "rm -rf dist"
62
+ "clean": "rm -rf dist",
63
+ "yalc": "yalc publish",
64
+ "yalc:push": "yalc push"
59
65
  }
60
66
  }