@qti-components/associate-interaction 1.1.7-next.4 → 2.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.
@@ -7,11 +7,15 @@ declare const QtiAssociateInteraction_base: (abstract new (...args: any[]) => im
7
7
  * @slot prompt - The prompt shown above the choices.
8
8
  * @slot qti-simple-associable-choice - The associable choice sources.
9
9
  *
10
- * @csspart drags - Wrapper around the associable choice slot.
10
+ * @csspart bank - The slot holding the unplaced chips (was `drags`).
11
11
  * @csspart drops - The container holding all drop targets.
12
- * @csspart drop-row - Container for one left/right drop pair.
13
- * @csspart drop - Each individual drop target.
12
+ * @csspart row - One left/right drop pair (was `drop-row`).
13
+ * @csspart drop - Each individual drop target. Tokens: `filled`, `full`, `drag-active`, `drag-over`
14
+ * (a plain div cannot carry `:state()`, so the state names are part tokens here).
14
15
  * @csspart message - Live validation message region (role="alert").
16
+ * @cssstate disabled - The interaction is switched off.
17
+ * @cssstate readonly - The interaction is frozen: it shows its value and takes no input.
18
+ * @cssstate drag-active - A drag is in progress (was `dragzone-active`, `dragzone-enabled`).
15
19
  */
16
20
  export declare class QtiAssociateInteraction extends QtiAssociateInteraction_base {
17
21
  #private;
@@ -9,7 +9,6 @@ import { repeat } from 'lit/directives/repeat.js';
9
9
  import { state } from 'lit/decorators.js';
10
10
  import { Interaction } from '@qti-components/base';
11
11
  import { DragDropSlottedMixin, DragDropSlottedSortableMixin } from '@qti-components/interactions-core/mixins/drag-drop-observables';
12
- // import { DragDropInteractionMixin } from '@qti-components/interactions-core/mixins/drag-drop';
13
12
  import styles from './qti-associate-interaction.styles';
14
13
  const SlottedBase = DragDropSlottedMixin(Interaction, 'qti-simple-associable-choice', `[part~='drop']`, `slot[name='qti-simple-associable-choice']`, 'pointerWithin');
15
14
  /**
@@ -18,13 +17,17 @@ const SlottedBase = DragDropSlottedMixin(Interaction, 'qti-simple-associable-cho
18
17
  * @slot prompt - The prompt shown above the choices.
19
18
  * @slot qti-simple-associable-choice - The associable choice sources.
20
19
  *
21
- * @csspart drags - Wrapper around the associable choice slot.
20
+ * @csspart bank - The slot holding the unplaced chips (was `drags`).
22
21
  * @csspart drops - The container holding all drop targets.
23
- * @csspart drop-row - Container for one left/right drop pair.
24
- * @csspart drop - Each individual drop target.
22
+ * @csspart row - One left/right drop pair (was `drop-row`).
23
+ * @csspart drop - Each individual drop target. Tokens: `filled`, `full`, `drag-active`, `drag-over`
24
+ * (a plain div cannot carry `:state()`, so the state names are part tokens here).
25
25
  * @csspart message - Live validation message region (role="alert").
26
+ * @cssstate disabled - The interaction is switched off.
27
+ * @cssstate readonly - The interaction is frozen: it shows its value and takes no input.
28
+ * @cssstate drag-active - A drag is in progress (was `dragzone-active`, `dragzone-enabled`).
26
29
  */
27
- export class QtiAssociateInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable="true"]') {
30
+ export class QtiAssociateInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[data-draggable="true"]') {
28
31
  static { this.styles = styles; }
29
32
  constructor() {
30
33
  super();
@@ -114,10 +117,10 @@ export class QtiAssociateInteraction extends DragDropSlottedSortableMixin(Slotte
114
117
  }
115
118
  render() {
116
119
  return html ` <slot name="prompt"></slot>
117
- <slot part="drags" name="qti-simple-associable-choice"></slot>
120
+ <slot part="bank drags" name="qti-simple-associable-choice"></slot>
118
121
  <div part="drops">
119
122
  ${this._childrenMap.length > 0 &&
120
- Array.from({ length: this.#rowCount }, (_, index) => html `<div part="drop-row">
123
+ Array.from({ length: this.#rowCount }, (_, index) => html `<div part="row drop-row">
121
124
  ${this.#renderDrop(`droplist${index}_left`, `left${index}`)}
122
125
  ${this.#renderDrop(`droplist${index}_right`, `right${index}`)}
123
126
  </div>`)}
@@ -1 +1 @@
1
- {"version":3,"file":"qti-associate-interaction.js","sourceRoot":"","sources":["../src/qti-associate-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { 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-associate-interaction.styles';\n\nimport type { QtiSimpleAssociableChoice } from '@qti-components/interactions-core/elements/qti-simple-associable-choice';\nimport type { CSSResultGroup } from 'lit';\nconst SlottedBase = DragDropSlottedMixin(\n Interaction,\n 'qti-simple-associable-choice',\n `[part~='drop']`,\n `slot[name='qti-simple-associable-choice']`,\n 'pointerWithin'\n);\n\n/**\n * Drag-and-drop associate interaction: candidates pair associable choices.\n *\n * @slot prompt - The prompt shown above the choices.\n * @slot qti-simple-associable-choice - The associable choice sources.\n *\n * @csspart drags - Wrapper around the associable choice slot.\n * @csspart drops - The container holding all drop targets.\n * @csspart drop-row - Container for one left/right drop pair.\n * @csspart drop - Each individual drop target.\n * @csspart message - Live validation message region (role=\"alert\").\n */\nexport class QtiAssociateInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[qti-draggable=\"true\"]') {\n static override styles: CSSResultGroup = styles;\n @state() protected _childrenMap: Element[] = [];\n\n protected _registerChoiceHandler: (event: CustomEvent) => void;\n\n constructor() {\n super();\n this._registerChoiceHandler = this._registerChoice.bind(this);\n this.addEventListener('register-qti-simple-associable-choice', this._registerChoiceHandler);\n }\n\n protected _registerChoice(event: CustomEvent) {\n // Use composedPath()[0] to get the actual origin — event.target is retargeted to `this`\n // when a composed event crosses the shadow DOM boundary, making it useless for filtering.\n const origin = event.composedPath()[0] as HTMLElement;\n // Ignore clones placed in shadow DOM drop zones — only register light DOM choices\n if (this.shadowRoot?.contains(origin)) return;\n const choice = origin as QtiSimpleAssociableChoice;\n if (!this._childrenMap.includes(choice)) {\n this._childrenMap = [...this._childrenMap, choice];\n }\n }\n\n /**\n * A drop target, holding whatever the interaction's placement map says it holds.\n * `data-declarative-drops` tells the mixin to keep the chips in that map rather than appending\n * them into this div.\n */\n #renderDrop(identifier: string, name: string) {\n const drags = this._dragDrop?.nodesByTarget?.[identifier] ?? [];\n return html`<div name=${name} part=\"drop\" data-declarative-drops identifier=${identifier}>\n ${repeat(\n drags,\n node => node.getAttribute('identifier'),\n node => node\n )}\n </div>`;\n }\n\n /**\n * How many association rows to render.\n *\n * `max-associations` is how many associations the candidate may *make*, so it is exactly the\n * number of rows. This used to be `ceil(choices / 2)` — the number of pairs the choices could\n * possibly form — which made the limit unenforceable: with six choices the interaction always\n * drew three rows, so `max-associations=\"3\"` could never be exceeded and its validation always\n * passed. Worse, the number read as a cap on *drags placed* rather than on associations formed.\n *\n * `0` is QTI's \"no limit\". With no limit there is still a natural ceiling: every choice paired\n * with one other.\n */\n get #rowCount(): number {\n return this.maxAssociations > 0 ? this.maxAssociations : Math.ceil(this._childrenMap.length / 2);\n }\n\n protected getResponse(): string[] {\n const response: string[] = [];\n // Same count as `render()`. Read fewer rows than were drawn and the pairs in the last rows\n // vanish from the response without a trace.\n for (let i = 0; i < this.#rowCount; i++) {\n const leftDrop = this.shadowRoot?.querySelector(`[part~='drop'][identifier=\"droplist${i}_left\"]`);\n const rightDrop = this.shadowRoot?.querySelector(`[part~='drop'][identifier=\"droplist${i}_right\"]`);\n // Read placement, not the DOM: the DOM is rendered from placement now.\n const leftId = leftDrop && this.chipsIn(leftDrop as HTMLElement)[0]?.getAttribute('identifier');\n const rightId = rightDrop && this.chipsIn(rightDrop as HTMLElement)[0]?.getAttribute('identifier');\n if (leftId && rightId) {\n response.push(`${leftId} ${rightId}`);\n }\n }\n return response;\n }\n\n /**\n * How many associations the candidate has made.\n *\n * The mixin counts placed chips, which is right for gap-match — one chip in one gap is one\n * association — and wrong here. An association is a *pair*, so three associations are six chips,\n * and `max-associations=\"3\"` compared 6 against 3 and declared the item invalid the moment it was\n * correctly filled in.\n *\n * A half-filled row is not an association yet, and `getResponse()` already knows that: it emits a\n * pair only when both sides hold a chip. Counting its entries counts associations by definition,\n * and keeps the number validation sees equal to the number the response reports.\n */\n public override totalAssociationsFromState(): number {\n // The mixin's version syncs first, and callers depend on that: `getResponse` reads the\n // placement map, which is stale until the sync runs.\n this.syncDragDropState();\n return this.getResponse().length;\n }\n\n getValue(val: string[]) {\n return (\n val?.flatMap((pair, i) => {\n const parts = pair.split(' ');\n if (parts.length !== 2) return [];\n return [`${parts[0]} droplist${i}_left`, `${parts[1]} droplist${i}_right`];\n }) ?? []\n );\n }\n\n override render() {\n return html` <slot name=\"prompt\"></slot>\n <slot part=\"drags\" name=\"qti-simple-associable-choice\"></slot>\n <div part=\"drops\">\n ${this._childrenMap.length > 0 &&\n Array.from(\n { length: this.#rowCount },\n (_, index) =>\n html`<div part=\"drop-row\">\n ${this.#renderDrop(`droplist${index}_left`, `left${index}`)}\n ${this.#renderDrop(`droplist${index}_right`, `right${index}`)}\n </div>`\n )}\n\n <div role=\"alert\" part=\"message\" id=\"validation-message\"></div>\n </div>`;\n }\n\n protected override updated(changedProperties: Map<string, unknown>): void {\n super.updated(changedProperties);\n if (changedProperties.has('_childrenMap')) {\n this.cacheInteractiveElements();\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener('register-qti-simple-associable-choice', this._registerChoiceHandler);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-associate-interaction': QtiAssociateInteraction;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-associate-interaction.js","sourceRoot":"","sources":["../src/qti-associate-interaction.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC7B,MAAM,gEAAgE,CAAC;AAExE,OAAO,MAAM,MAAM,oCAAoC,CAAC;AAIxD,MAAM,WAAW,GAAG,oBAAoB,CACtC,WAAW,EACX,8BAA8B,EAC9B,gBAAgB,EAChB,2CAA2C,EAC3C,eAAe,CAChB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,uBAAwB,SAAQ,4BAA4B,CAAC,WAAW,EAAE,yBAAyB,CAAC;aAC/F,WAAM,GAAmB,MAAM,AAAzB,CAA0B;IAKhD;QACE,KAAK,EAAE,CAAC;QALS,iBAAY,GAAc,EAAE,CAAC;QAM9C,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9D,IAAI,CAAC,gBAAgB,CAAC,uCAAuC,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAC9F,CAAC;IAES,eAAe,CAAC,KAAkB;QAC1C,wFAAwF;QACxF,0FAA0F;QAC1F,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,CAAgB,CAAC;QACtD,kFAAkF;QAClF,IAAI,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QAC9C,MAAM,MAAM,GAAG,MAAmC,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,YAAY,GAAG,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,UAAkB,EAAE,IAAY;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QAChE,OAAO,IAAI,CAAA,aAAa,IAAI,kDAAkD,UAAU;QACpF,MAAM,CACN,KAAK,EACL,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EACvC,IAAI,CAAC,EAAE,CAAC,IAAI,CACb;WACI,CAAC;IACV,CAAC;IAED;;;;;;;;;;;OAWG;IACH,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnG,CAAC;IAES,WAAW;QACnB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,2FAA2F;QAC3F,4CAA4C;QAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,sCAAsC,CAAC,SAAS,CAAC,CAAC;YAClG,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,sCAAsC,CAAC,UAAU,CAAC,CAAC;YACpG,uEAAuE;YACvE,MAAM,MAAM,GAAG,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,QAAuB,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC;YAChG,MAAM,OAAO,GAAG,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,SAAwB,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC;YACnG,IAAI,MAAM,IAAI,OAAO,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;;;;;;;;OAWG;IACa,0BAA0B;QACxC,uFAAuF;QACvF,qDAAqD;QACrD,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC;IACnC,CAAC;IAED,QAAQ,CAAC,GAAa;QACpB,OAAO,CACL,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAClC,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7E,CAAC,CAAC,IAAI,EAAE,CACT,CAAC;IACJ,CAAC;IAEQ,MAAM;QACb,OAAO,IAAI,CAAA;;;UAGL,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;YAC9B,KAAK,CAAC,IAAI,CACR,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,EAC1B,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CACX,IAAI,CAAA;gBACA,IAAI,CAAC,WAAW,CAAC,WAAW,KAAK,OAAO,EAAE,OAAO,KAAK,EAAE,CAAC;gBACzD,IAAI,CAAC,WAAW,CAAC,WAAW,KAAK,QAAQ,EAAE,QAAQ,KAAK,EAAE,CAAC;mBACxD,CACV;;;aAGI,CAAC;IACZ,CAAC;IAEkB,OAAO,CAAC,iBAAuC;QAChE,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QACjC,IAAI,iBAAiB,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,wBAAwB,EAAE,CAAC;QAClC,CAAC;IACH,CAAC;IAEQ,oBAAoB;QAC3B,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAC7B,IAAI,CAAC,mBAAmB,CAAC,uCAAuC,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACjG,CAAC;;AAhIkB;IAAlB,KAAK,EAAE;6DAAwC","sourcesContent":["import { html } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { 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-associate-interaction.styles';\n\nimport type { QtiSimpleAssociableChoice } from '@qti-components/interactions-core/elements/qti-simple-associable-choice';\nimport type { CSSResultGroup } from 'lit';\nconst SlottedBase = DragDropSlottedMixin(\n Interaction,\n 'qti-simple-associable-choice',\n `[part~='drop']`,\n `slot[name='qti-simple-associable-choice']`,\n 'pointerWithin'\n);\n\n/**\n * Drag-and-drop associate interaction: candidates pair associable choices.\n *\n * @slot prompt - The prompt shown above the choices.\n * @slot qti-simple-associable-choice - The associable choice sources.\n *\n * @csspart bank - The slot holding the unplaced chips (was `drags`).\n * @csspart drops - The container holding all drop targets.\n * @csspart row - One left/right drop pair (was `drop-row`).\n * @csspart drop - Each individual drop target. Tokens: `filled`, `full`, `drag-active`, `drag-over`\n * (a plain div cannot carry `:state()`, so the state names are part tokens here).\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 QtiAssociateInteraction extends DragDropSlottedSortableMixin(SlottedBase, '[data-draggable=\"true\"]') {\n static override styles: CSSResultGroup = styles;\n @state() protected _childrenMap: Element[] = [];\n\n protected _registerChoiceHandler: (event: CustomEvent) => void;\n\n constructor() {\n super();\n this._registerChoiceHandler = this._registerChoice.bind(this);\n this.addEventListener('register-qti-simple-associable-choice', this._registerChoiceHandler);\n }\n\n protected _registerChoice(event: CustomEvent) {\n // Use composedPath()[0] to get the actual origin — event.target is retargeted to `this`\n // when a composed event crosses the shadow DOM boundary, making it useless for filtering.\n const origin = event.composedPath()[0] as HTMLElement;\n // Ignore clones placed in shadow DOM drop zones — only register light DOM choices\n if (this.shadowRoot?.contains(origin)) return;\n const choice = origin as QtiSimpleAssociableChoice;\n if (!this._childrenMap.includes(choice)) {\n this._childrenMap = [...this._childrenMap, choice];\n }\n }\n\n /**\n * A drop target, holding whatever the interaction's placement map says it holds.\n * `data-declarative-drops` tells the mixin to keep the chips in that map rather than appending\n * them into this div.\n */\n #renderDrop(identifier: string, name: string) {\n const drags = this._dragDrop?.nodesByTarget?.[identifier] ?? [];\n return html`<div name=${name} part=\"drop\" data-declarative-drops identifier=${identifier}>\n ${repeat(\n drags,\n node => node.getAttribute('identifier'),\n node => node\n )}\n </div>`;\n }\n\n /**\n * How many association rows to render.\n *\n * `max-associations` is how many associations the candidate may *make*, so it is exactly the\n * number of rows. This used to be `ceil(choices / 2)` — the number of pairs the choices could\n * possibly form — which made the limit unenforceable: with six choices the interaction always\n * drew three rows, so `max-associations=\"3\"` could never be exceeded and its validation always\n * passed. Worse, the number read as a cap on *drags placed* rather than on associations formed.\n *\n * `0` is QTI's \"no limit\". With no limit there is still a natural ceiling: every choice paired\n * with one other.\n */\n get #rowCount(): number {\n return this.maxAssociations > 0 ? this.maxAssociations : Math.ceil(this._childrenMap.length / 2);\n }\n\n protected getResponse(): string[] {\n const response: string[] = [];\n // Same count as `render()`. Read fewer rows than were drawn and the pairs in the last rows\n // vanish from the response without a trace.\n for (let i = 0; i < this.#rowCount; i++) {\n const leftDrop = this.shadowRoot?.querySelector(`[part~='drop'][identifier=\"droplist${i}_left\"]`);\n const rightDrop = this.shadowRoot?.querySelector(`[part~='drop'][identifier=\"droplist${i}_right\"]`);\n // Read placement, not the DOM: the DOM is rendered from placement now.\n const leftId = leftDrop && this.chipsIn(leftDrop as HTMLElement)[0]?.getAttribute('identifier');\n const rightId = rightDrop && this.chipsIn(rightDrop as HTMLElement)[0]?.getAttribute('identifier');\n if (leftId && rightId) {\n response.push(`${leftId} ${rightId}`);\n }\n }\n return response;\n }\n\n /**\n * How many associations the candidate has made.\n *\n * The mixin counts placed chips, which is right for gap-match — one chip in one gap is one\n * association — and wrong here. An association is a *pair*, so three associations are six chips,\n * and `max-associations=\"3\"` compared 6 against 3 and declared the item invalid the moment it was\n * correctly filled in.\n *\n * A half-filled row is not an association yet, and `getResponse()` already knows that: it emits a\n * pair only when both sides hold a chip. Counting its entries counts associations by definition,\n * and keeps the number validation sees equal to the number the response reports.\n */\n public override totalAssociationsFromState(): number {\n // The mixin's version syncs first, and callers depend on that: `getResponse` reads the\n // placement map, which is stale until the sync runs.\n this.syncDragDropState();\n return this.getResponse().length;\n }\n\n getValue(val: string[]) {\n return (\n val?.flatMap((pair, i) => {\n const parts = pair.split(' ');\n if (parts.length !== 2) return [];\n return [`${parts[0]} droplist${i}_left`, `${parts[1]} droplist${i}_right`];\n }) ?? []\n );\n }\n\n override render() {\n return html` <slot name=\"prompt\"></slot>\n <slot part=\"bank drags\" name=\"qti-simple-associable-choice\"></slot>\n <div part=\"drops\">\n ${this._childrenMap.length > 0 &&\n Array.from(\n { length: this.#rowCount },\n (_, index) =>\n html`<div part=\"row drop-row\">\n ${this.#renderDrop(`droplist${index}_left`, `left${index}`)}\n ${this.#renderDrop(`droplist${index}_right`, `right${index}`)}\n </div>`\n )}\n\n <div role=\"alert\" part=\"message\" id=\"validation-message\"></div>\n </div>`;\n }\n\n protected override updated(changedProperties: Map<string, unknown>): void {\n super.updated(changedProperties);\n if (changedProperties.has('_childrenMap')) {\n this.cacheInteractiveElements();\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener('register-qti-simple-associable-choice', this._registerChoiceHandler);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-associate-interaction': QtiAssociateInteraction;\n }\n}\n"]}
@@ -21,15 +21,19 @@ export default [
21
21
  then compresses the chips instead of moving them down: a bank of six squeezed the last chip
22
22
  until only its grip was left, at no width the author could see coming. A chip is a fixed
23
23
  thing, so the only place the overflow can go is the next line. */
24
- slot[part~='drags'] {
24
+ [part~='bank'] {
25
+ /* The bank, the same in every drag-drop interaction: flush with the container (no padding, no
26
+ margin, no edge of its own), chips a --qti-gap apart. Its lit and ringed looks while a chip is
27
+ in flight are the theme's ('::part(bank drag-active)', '::part(bank drag-over)'). */
25
28
  display: flex;
26
29
  flex-wrap: wrap;
27
30
  align-items: flex-start;
28
31
  flex: 1;
29
- border: 2px solid transparent;
30
- margin: 1rem 0;
31
- border-radius: 0.3rem;
32
- gap: 0.5rem;
32
+ gap: var(--qti-gap, 0.5rem);
33
+ padding: 0;
34
+ margin: 0;
35
+ border: 0;
36
+ border-radius: var(--qti-radius-box, 0.2rem);
33
37
  }
34
38
 
35
39
  /* Drop target. --qti-dropzone-min-height is published by the interaction from the measured
@@ -49,7 +53,7 @@ export default [
49
53
  gap: 0.5rem;
50
54
  }
51
55
 
52
- :host::part(drop-row) {
56
+ :host::part(row) {
53
57
  display: flex;
54
58
  justify-content: space-between;
55
59
  background: linear-gradient(
@@ -1 +1 @@
1
- {"version":3,"file":"qti-associate-interaction.styles.js","sourceRoot":"","sources":["../src/qti-associate-interaction.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEhF;;;;;;;;GAQG;AAEH,eAAe;IACb,SAAS;IACT,iBAAiB;IACjB,UAAU;IACV,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDF;CACF,CAAC","sourcesContent":["import { css } from 'lit';\n\nimport { boxSizing, dropRegion, validationMessage } from '@qti-components/base';\n\n/**\n * Layout and sizing only. Paint and state live in the theme.\n *\n * `[part~='drop'][active]` and `[part~='drop'][enabled]` moved to\n * `styles/qti-theme/interactions/qti-associate-interaction.css` as `::part(active)` /\n * `::part(enabled)`, since an attribute selector cannot follow `::part()` and a <div> cannot carry\n * a custom state. Both carried `!important`, which only existed to beat the shadow's own rules from\n * the shadow. Neither needs it from the theme.\n */\n\nexport default [\n boxSizing,\n validationMessage,\n dropRegion,\n css`\n :host {\n display: block; /* necessary to calculate scaling position */\n }\n /* The bank WRAPS. Without flex-wrap the row is nowrap, and a flex item's default flex-shrink: 1\n then compresses the chips instead of moving them down: a bank of six squeezed the last chip\n until only its grip was left, at no width the author could see coming. A chip is a fixed\n thing, so the only place the overflow can go is the next line. */\n slot[part~='drags'] {\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n flex: 1;\n border: 2px solid transparent;\n margin: 1rem 0;\n border-radius: 0.3rem;\n gap: 0.5rem;\n }\n\n /* Drop target. --qti-dropzone-min-height is published by the interaction from the measured\n chip height; 3rem is the fallback when there is nothing to measure. Was an inline style. */\n [part~='drop'] {\n display: grid;\n min-height: var(--qti-dropzone-min-height, 3rem);\n min-width: var(--qti-dropzone-min-width, 3rem);\n border: 1px solid var(--qti-border-color, #d1d5db);\n border-radius: 0.25rem;\n overflow: hidden;\n }\n\n [part='drops'] {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n }\n\n :host::part(drop-row) {\n display: flex;\n justify-content: space-between;\n background: linear-gradient(\n 180deg,\n rgb(0 0 0 / 0%) calc(50% - 1px),\n var(--qti-border-color) calc(50%),\n rgb(0 0 0 / 0%) calc(50% + 1px)\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]) ::slotted(qti-simple-associable-choice) {\n display: none;\n }\n `\n];\n"]}
1
+ {"version":3,"file":"qti-associate-interaction.styles.js","sourceRoot":"","sources":["../src/qti-associate-interaction.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEhF;;;;;;;;GAQG;AAEH,eAAe;IACb,SAAS;IACT,iBAAiB;IACjB,UAAU;IACV,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDF;CACF,CAAC","sourcesContent":["import { css } from 'lit';\n\nimport { boxSizing, dropRegion, validationMessage } from '@qti-components/base';\n\n/**\n * Layout and sizing only. Paint and state live in the theme.\n *\n * `[part~='drop'][active]` and `[part~='drop'][enabled]` moved to\n * `styles/qti-theme/interactions/qti-associate-interaction.css` as `::part(active)` /\n * `::part(enabled)`, since an attribute selector cannot follow `::part()` and a <div> cannot carry\n * a custom state. Both carried `!important`, which only existed to beat the shadow's own rules from\n * the shadow. Neither needs it from the theme.\n */\n\nexport default [\n boxSizing,\n validationMessage,\n dropRegion,\n css`\n :host {\n display: block; /* necessary to calculate scaling position */\n }\n /* The bank WRAPS. Without flex-wrap the row is nowrap, and a flex item's default flex-shrink: 1\n then compresses the chips instead of moving them down: a bank of six squeezed the last chip\n until only its grip was left, at no width the author could see coming. A chip is a fixed\n thing, so the only place the overflow can go is the next line. */\n [part~='bank'] {\n /* The bank, the same in every drag-drop interaction: flush with the container (no padding, no\n margin, no edge of its own), chips a --qti-gap apart. Its lit and ringed looks while a chip is\n in flight are the theme's ('::part(bank drag-active)', '::part(bank drag-over)'). */\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n flex: 1;\n gap: var(--qti-gap, 0.5rem);\n padding: 0;\n margin: 0;\n border: 0;\n border-radius: var(--qti-radius-box, 0.2rem);\n }\n\n /* Drop target. --qti-dropzone-min-height is published by the interaction from the measured\n chip height; 3rem is the fallback when there is nothing to measure. Was an inline style. */\n [part~='drop'] {\n display: grid;\n min-height: var(--qti-dropzone-min-height, 3rem);\n min-width: var(--qti-dropzone-min-width, 3rem);\n border: 1px solid var(--qti-border-color, #d1d5db);\n border-radius: 0.25rem;\n overflow: hidden;\n }\n\n [part='drops'] {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n }\n\n :host::part(row) {\n display: flex;\n justify-content: space-between;\n background: linear-gradient(\n 180deg,\n rgb(0 0 0 / 0%) calc(50% - 1px),\n var(--qti-border-color) calc(50%),\n rgb(0 0 0 / 0%) calc(50% + 1px)\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]) ::slotted(qti-simple-associable-choice) {\n display: none;\n }\n `\n];\n"]}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@qti-components/associate-interaction",
3
3
  "description": "QTI associate-interaction interaction",
4
- "version": "1.1.7-next.4",
4
+ "version": "2.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": {