@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.
- package/dist/elements.d.ts +6 -0
- package/dist/elements.js +4 -0
- package/dist/elements.js.map +1 -0
- package/dist/qti-match-interaction-tabular.styles.d.ts +8 -0
- package/dist/qti-match-interaction-tabular.styles.js +69 -0
- package/dist/qti-match-interaction-tabular.styles.js.map +1 -0
- package/dist/qti-match-interaction.d.ts +63 -8
- package/dist/qti-match-interaction.js +176 -145
- package/dist/qti-match-interaction.js.map +1 -1
- package/dist/qti-match-interaction.styles.d.ts +1 -1
- package/dist/qti-match-interaction.styles.js +112 -27
- package/dist/qti-match-interaction.styles.js.map +1 -1
- package/dist/register.js +6 -2
- package/dist/register.js.map +1 -1
- package/package.json +11 -5
package/dist/elements.js
ADDED
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
158
|
+
this.syncTabularSlotting();
|
|
85
159
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
149
|
-
this.correctOptions = matches || [];
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
this.correctOptions = null;
|
|
153
|
-
}
|
|
203
|
+
return super.validate();
|
|
154
204
|
}
|
|
155
205
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
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 {
|
|
2
|
-
|
|
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
|
package/dist/register.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
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": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"author": "",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@qti-components/base": "^
|
|
8
|
-
"@qti-components/
|
|
9
|
-
"@qti-components/
|
|
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
|
}
|