@qti-components/match-interaction 3.0.0-next.4 → 3.0.0-next.5

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.
@@ -45,19 +45,19 @@ export default [
45
45
  outer grid's tracks. This structure is shared with the editor's tabular
46
46
  template so both can inherit the same theme CSS. No per-choice inline
47
47
  style writes are needed — auto-flow + subgrid handle placement. */
48
- :host(.qti-match-tabular) [part='cols-wrap'] {
48
+ :host(.qti-match-tabular) [part~='columns'] {
49
49
  grid-column: 2 / -1;
50
50
  grid-row: 1;
51
51
  display: grid;
52
52
  grid-template-columns: subgrid;
53
53
  }
54
- :host(.qti-match-tabular) [part='rows-wrap'] {
54
+ :host(.qti-match-tabular) [part~='rows'] {
55
55
  grid-column: 1;
56
56
  grid-row: 2 / -1;
57
57
  display: grid;
58
58
  grid-template-rows: subgrid;
59
59
  }
60
- :host(.qti-match-tabular) [part='checkbox-grid'] {
60
+ :host(.qti-match-tabular) [part~='cells'] {
61
61
  grid-column: 2 / -1;
62
62
  grid-row: 2 / -1;
63
63
  display: grid;
@@ -75,10 +75,10 @@ export default [
75
75
  :host(.qti-match-tabular.qti-header-hidden) [part='grid'] {
76
76
  grid-template-rows: repeat(var(--qti-match-rows, 1), auto);
77
77
  }
78
- :host(.qti-match-tabular.qti-header-hidden) :is([part='corner'], [part='cols-wrap']) {
78
+ :host(.qti-match-tabular.qti-header-hidden) :is([part='corner'], [part~='columns']) {
79
79
  display: none;
80
80
  }
81
- :host(.qti-match-tabular.qti-header-hidden) :is([part='rows-wrap'], [part='checkbox-grid']) {
81
+ :host(.qti-match-tabular.qti-header-hidden) :is([part~='rows'], [part~='cells']) {
82
82
  grid-row: 1 / -1;
83
83
  }
84
84
 
@@ -1 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyFF;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 /*\n * Each cell's radio or checkbox is a real input, visually hidden over the whole cell: it takes\n * Tab focus, the arrow keys move within a row's radio group and Space toggles a checkbox, all\n * natively. It was the hidden attribute, which took it out of the tab order, so the grid could not be\n * answered from a keyboard at all. The span beside it draws the control; the theme rings the\n * cell (::part(input-cell):focus-within).\n */\n :host(.qti-match-tabular) [part~='input-cell'] {\n position: relative;\n }\n :host(.qti-match-tabular) [part~='input-cell'] > input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\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 * qti-header-hidden (vocabulary §1.2.8.2): \"Do not display the top row of the table where the\n * column headers are displayed.\" The whole row goes, the top-left cell with it.\n */\n :host(.qti-match-tabular.qti-header-hidden) [part='grid'] {\n grid-template-rows: repeat(var(--qti-match-rows, 1), auto);\n }\n :host(.qti-match-tabular.qti-header-hidden) :is([part='corner'], [part='cols-wrap']) {\n display: none;\n }\n :host(.qti-match-tabular.qti-header-hidden) :is([part='rows-wrap'], [part='checkbox-grid']) {\n grid-row: 1 / -1;\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
+ {"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyFF;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 /*\n * Each cell's radio or checkbox is a real input, visually hidden over the whole cell: it takes\n * Tab focus, the arrow keys move within a row's radio group and Space toggles a checkbox, all\n * natively. It was the hidden attribute, which took it out of the tab order, so the grid could not be\n * answered from a keyboard at all. The span beside it draws the control; the theme rings the\n * cell (::part(input-cell):focus-within).\n */\n :host(.qti-match-tabular) [part~='input-cell'] {\n position: relative;\n }\n :host(.qti-match-tabular) [part~='input-cell'] > input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\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~='columns'] {\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'] {\n grid-column: 1;\n grid-row: 2 / -1;\n display: grid;\n grid-template-rows: subgrid;\n }\n :host(.qti-match-tabular) [part~='cells'] {\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 * qti-header-hidden (vocabulary §1.2.8.2): \"Do not display the top row of the table where the\n * column headers are displayed.\" The whole row goes, the top-left cell with it.\n */\n :host(.qti-match-tabular.qti-header-hidden) [part='grid'] {\n grid-template-rows: repeat(var(--qti-match-rows, 1), auto);\n }\n :host(.qti-match-tabular.qti-header-hidden) :is([part='corner'], [part~='columns']) {\n display: none;\n }\n :host(.qti-match-tabular.qti-header-hidden) :is([part~='rows'], [part~='cells']) {\n grid-row: 1 / -1;\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"]}
@@ -33,18 +33,21 @@ declare const QtiMatchInteraction_base: (abstract new (...args: any[]) => import
33
33
  *
34
34
  * @csspart grid - The tabular grid container.
35
35
  * @csspart corner - The top-left corner cell of the grid; shows `data-first-column-header`.
36
- * @csspart cols-wrap - Wrapper row for the column headers.
37
- * @csspart c-header - The column header slot wrapper.
38
- * @csspart rows-wrap - Wrapper column for the row headers.
39
- * @csspart r-header - The row header slot wrapper.
40
- * @csspart checkbox-grid - The interactive cell grid area.
41
- * @csspart input-cell - Each cell's label element.
42
- * @csspart control - The visible checkbox/radio box (tabular mode). Also carries the variant
43
- * tokens `radio` | `checkbox`, `checked`, and `correct` | `incorrect` — e.g.
44
- * `::part(control radio checked correct)`. These mirror the custom-state vocabulary; the cells
45
- * are plain spans in the shadow root, so they cannot carry real `:state()`.
46
- * @csspart control-mark - The inner mark element; carries the same variant tokens as `control`.
36
+ * @csspart columns - Wrapper row for the column headers (was `cols-wrap`).
37
+ * @csspart column-header - The column header slot (was `c-header`).
38
+ * @csspart rows - Wrapper column for the row headers (was `rows-wrap`).
39
+ * @csspart row-header - The row header slot (was `r-header`).
40
+ * @csspart cells - The interactive cell grid area (was `checkbox-grid`).
41
+ * @csspart cell - Each cell's label element (was `input-cell`).
42
+ * @csspart control - The visible checkbox/radio box (tabular mode). Tokens: `radio` | `checkbox`,
43
+ * `checked`, `candidate-correct` | `candidate-incorrect` — e.g.
44
+ * `::part(control radio checked candidate-correct)`. These mirror the custom-state vocabulary;
45
+ * the cells are plain spans in the shadow root, so they cannot carry real `:state()`.
46
+ * @csspart control-mark - The inner mark element; carries the same tokens as `control`.
47
47
  * @csspart message - Live validation message region (role="alert").
48
+ * @cssstate disabled - The interaction is switched off.
49
+ * @cssstate readonly - The interaction is frozen: it shows its value and takes no input.
50
+ * @cssstate drag-active - A drag is in progress (was `dragzone-active`, `dragzone-enabled`).
48
51
  */
49
52
  export declare class QtiMatchInteraction extends QtiMatchInteraction_base {
50
53
  static styles: CSSResultGroup;
@@ -8,7 +8,6 @@ import { html, nothing } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
9
  import { Interaction } from '@qti-components/base';
10
10
  import { DragDropSlottedMixin, DragDropSlottedSortableMixin } from '@qti-components/interactions-core/mixins/drag-drop-observables';
11
- // import { DragDropInteractionMixin } from '@qti-components/interactions-core/mixins/drag-drop';
12
11
  import styles from './qti-match-interaction.styles';
13
12
  import tabularStyles from './qti-match-interaction-tabular.styles';
14
13
  /**
@@ -42,20 +41,23 @@ import tabularStyles from './qti-match-interaction-tabular.styles';
42
41
  *
43
42
  * @csspart grid - The tabular grid container.
44
43
  * @csspart corner - The top-left corner cell of the grid; shows `data-first-column-header`.
45
- * @csspart cols-wrap - Wrapper row for the column headers.
46
- * @csspart c-header - The column header slot wrapper.
47
- * @csspart rows-wrap - Wrapper column for the row headers.
48
- * @csspart r-header - The row header slot wrapper.
49
- * @csspart checkbox-grid - The interactive cell grid area.
50
- * @csspart input-cell - Each cell's label element.
51
- * @csspart control - The visible checkbox/radio box (tabular mode). Also carries the variant
52
- * tokens `radio` | `checkbox`, `checked`, and `correct` | `incorrect` — e.g.
53
- * `::part(control radio checked correct)`. These mirror the custom-state vocabulary; the cells
54
- * are plain spans in the shadow root, so they cannot carry real `:state()`.
55
- * @csspart control-mark - The inner mark element; carries the same variant tokens as `control`.
44
+ * @csspart columns - Wrapper row for the column headers (was `cols-wrap`).
45
+ * @csspart column-header - The column header slot (was `c-header`).
46
+ * @csspart rows - Wrapper column for the row headers (was `rows-wrap`).
47
+ * @csspart row-header - The row header slot (was `r-header`).
48
+ * @csspart cells - The interactive cell grid area (was `checkbox-grid`).
49
+ * @csspart cell - Each cell's label element (was `input-cell`).
50
+ * @csspart control - The visible checkbox/radio box (tabular mode). Tokens: `radio` | `checkbox`,
51
+ * `checked`, `candidate-correct` | `candidate-incorrect` — e.g.
52
+ * `::part(control radio checked candidate-correct)`. These mirror the custom-state vocabulary;
53
+ * the cells are plain spans in the shadow root, so they cannot carry real `:state()`.
54
+ * @csspart control-mark - The inner mark element; carries the same tokens as `control`.
56
55
  * @csspart message - Live validation message region (role="alert").
56
+ * @cssstate disabled - The interaction is switched off.
57
+ * @cssstate readonly - The interaction is frozen: it shows its value and takes no input.
58
+ * @cssstate drag-active - A drag is in progress (was `dragzone-active`, `dragzone-enabled`).
57
59
  */
58
- 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"]') {
60
+ 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'), '[data-draggable="true"]') {
59
61
  constructor() {
60
62
  super(...arguments);
61
63
  /**
@@ -236,9 +238,11 @@ export class QtiMatchInteraction extends DragDropSlottedSortableMixin(DragDropSl
236
238
  style="--qti-match-rows: ${this.sourceChoices.length}; --qti-match-cols: ${this.targetChoices.length};"
237
239
  >
238
240
  <div part="corner">${this.dataFirstColumnHeader ?? nothing}</div>
239
- <div part="cols-wrap" role="row"><slot name="match-cols" part="c-header"></slot></div>
240
- <div part="rows-wrap"><slot name="match-rows" part="r-header"></slot></div>
241
- <div part="checkbox-grid">
241
+ <div part="columns cols-wrap" role="row">
242
+ <slot name="match-cols" part="column-header c-header"></slot>
243
+ </div>
244
+ <div part="rows rows-wrap"><slot name="match-rows" part="row-header r-header"></slot></div>
245
+ <div part="cells checkbox-grid">
242
246
  ${this.sourceChoices.map((row, r) => html `
243
247
  <div role="row" style="display: contents;">
244
248
  ${this.targetChoices.map((col, c) => {
@@ -267,7 +271,7 @@ export class QtiMatchInteraction extends DragDropSlottedSortableMixin(DragDropSl
267
271
  : row.matchMax !== 0 && selectedInRowCount >= row.matchMax && !checked;
268
272
  return html `
269
273
  <label
270
- part="input-cell"
274
+ part="cell input-cell"
271
275
  role="gridcell"
272
276
  style="grid-row: ${r + 1}; grid-column: ${c +
273
277
  1}; display: flex; align-items: center; justify-content: center; cursor: pointer;"
@@ -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';\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=1] - Maximum pairings across the whole interaction; `0`\n * means unlimited. Without the attribute it is QTI's default of 1 (information model §5.89).\n * @attr {number} [min-associations=0] - Minimum pairings for a valid response (QTI's default).\n * @attr {string} data-first-column-header - QTI shared vocabulary, with `qti-match-tabular`: the\n * text of the table's top-left cell.\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-header-hidden'|'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, and with it `qti-header-hidden` leaves out the column-header row;\n * 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; shows `data-first-column-header`.\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 /** QTI's defaults (information model §5.89). */\n public override maxAssociations = 1;\n public override minAssociations = 0;\n\n protected sourceChoices: QtiSimpleAssociableChoice[];\n protected targetChoices: QtiSimpleAssociableChoice[];\n protected lastCheckedRadio: HTMLInputElement | null = null;\n\n @property({ type: String }) class: string = '';\n\n @property({ type: String, attribute: 'data-first-column-header' }) dataFirstColumnHeader: 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 // A header labels its row or column and does nothing when activated, so it leaves the tab\n // order: Tab goes from one answer cell to the next, not through every label first.\n this.sourceChoices?.forEach(c => {\n stripSizing(c);\n c.setAttribute('role', 'rowheader');\n c.tabIndex = -1;\n });\n this.targetChoices?.forEach(c => {\n stripSizing(c);\n c.setAttribute('role', 'columnheader');\n c.tabIndex = -1;\n });\n } else {\n sourceSet?.removeAttribute('slot');\n targetSet?.removeAttribute('slot');\n this.sourceChoices?.forEach(c => {\n c.removeAttribute('role');\n c.tabIndex = c.disabled ? -1 : 0;\n });\n this.targetChoices?.forEach(c => {\n c.removeAttribute('role');\n c.tabIndex = c.disabled ? -1 : 0;\n });\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\">${this.dataFirstColumnHeader ?? nothing}</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 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
+ {"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\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=1] - Maximum pairings across the whole interaction; `0`\n * means unlimited. Without the attribute it is QTI's default of 1 (information model §5.89).\n * @attr {number} [min-associations=0] - Minimum pairings for a valid response (QTI's default).\n * @attr {string} data-first-column-header - QTI shared vocabulary, with `qti-match-tabular`: the\n * text of the table's top-left cell.\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-header-hidden'|'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, and with it `qti-header-hidden` leaves out the column-header row;\n * 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; shows `data-first-column-header`.\n * @csspart columns - Wrapper row for the column headers (was `cols-wrap`).\n * @csspart column-header - The column header slot (was `c-header`).\n * @csspart rows - Wrapper column for the row headers (was `rows-wrap`).\n * @csspart row-header - The row header slot (was `r-header`).\n * @csspart cells - The interactive cell grid area (was `checkbox-grid`).\n * @csspart cell - Each cell's label element (was `input-cell`).\n * @csspart control - The visible checkbox/radio box (tabular mode). Tokens: `radio` | `checkbox`,\n * `checked`, `candidate-correct` | `candidate-incorrect` — e.g.\n * `::part(control radio checked candidate-correct)`. These mirror the custom-state vocabulary;\n * the cells are plain spans in the shadow root, so they cannot carry real `:state()`.\n * @csspart control-mark - The inner mark element; carries the same tokens as `control`.\n * @csspart message - Live validation message region (role=\"alert\").\n * @cssstate disabled - The interaction is switched off.\n * @cssstate readonly - The interaction is frozen: it shows its value and takes no input.\n * @cssstate drag-active - A drag is in progress (was `dragzone-active`, `dragzone-enabled`).\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 '[data-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 /** QTI's defaults (information model §5.89). */\n public override maxAssociations = 1;\n public override minAssociations = 0;\n\n protected sourceChoices: QtiSimpleAssociableChoice[];\n protected targetChoices: QtiSimpleAssociableChoice[];\n protected lastCheckedRadio: HTMLInputElement | null = null;\n\n @property({ type: String }) class: string = '';\n\n @property({ type: String, attribute: 'data-first-column-header' }) dataFirstColumnHeader: 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 // A header labels its row or column and does nothing when activated, so it leaves the tab\n // order: Tab goes from one answer cell to the next, not through every label first.\n this.sourceChoices?.forEach(c => {\n stripSizing(c);\n c.setAttribute('role', 'rowheader');\n c.tabIndex = -1;\n });\n this.targetChoices?.forEach(c => {\n stripSizing(c);\n c.setAttribute('role', 'columnheader');\n c.tabIndex = -1;\n });\n } else {\n sourceSet?.removeAttribute('slot');\n targetSet?.removeAttribute('slot');\n this.sourceChoices?.forEach(c => {\n c.removeAttribute('role');\n c.tabIndex = c.disabled ? -1 : 0;\n });\n this.targetChoices?.forEach(c => {\n c.removeAttribute('role');\n c.tabIndex = c.disabled ? -1 : 0;\n });\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\">${this.dataFirstColumnHeader ?? nothing}</div>\n <div part=\"columns cols-wrap\" role=\"row\">\n <slot name=\"match-cols\" part=\"column-header c-header\"></slot>\n </div>\n <div part=\"rows rows-wrap\"><slot name=\"match-rows\" part=\"row-header r-header\"></slot></div>\n <div part=\"cells 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=\"cell 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 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"]}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@qti-components/match-interaction",
3
3
  "description": "QTI match-interaction interaction",
4
- "version": "3.0.0-next.4",
4
+ "version": "3.0.0-next.5",
5
5
  "author": "",
6
6
  "dependencies": {
7
- "@qti-components/base": "^3.0.0-next.5",
8
- "@qti-components/interactions-core": "^3.0.0-next.6",
7
+ "@qti-components/base": "^3.0.0-next.6",
8
+ "@qti-components/interactions-core": "^3.0.0-next.7",
9
9
  "@qti-components/utilities": "^1.3.3"
10
10
  },
11
11
  "devDependencies": {