@qti-components/base 3.0.0-next.5 → 3.0.0-next.6

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.
@@ -4,6 +4,10 @@ import type { InteractionContext } from '../context/interaction.context';
4
4
  import type { ConfigContext, ValidationDisplayMode } from '../context/config.context';
5
5
  import type { ValidatableInteraction } from '../lib/interaction.interface';
6
6
  import type { ResponseVariable } from '../lib/variables';
7
+ /** Why an interaction is invalid: too few (`min`), too many (`max`), or something else (`other`). */
8
+ export type ValidityKind = 'min' | 'max' | 'other';
9
+ /** What asked for the report: the candidate changing the response, or a submit (end attempt, form). */
10
+ export type ValidityTrigger = 'change' | 'submit';
7
11
  /**
8
12
  * Shared QTI interaction base. Formative correction behavior is supplied by the
9
13
  * `@qti-components/corrections` package.
@@ -53,7 +57,27 @@ export declare abstract class Interaction extends LitElement implements Validata
53
57
  set value(val: string | null);
54
58
  abstract get response(): string | string[] | null;
55
59
  abstract set response(val: string | string[] | null);
56
- reportValidity(): boolean;
60
+ /** Why the interaction is invalid right now; `undefined` while it is valid. */
61
+ protected _validityKind?: ValidityKind;
62
+ /** Set once a submit has reported; from then on every change reports in full. */
63
+ protected _validityReported: boolean;
64
+ /** True while this element asks its own internals to report, so its `invalid` is not a submit. */
65
+ protected _reportingNatively: boolean;
66
+ /** Forget that a submit has reported: a reset or a new attempt waits for a submit again. */
67
+ protected resetValidityReport(): void;
68
+ /**
69
+ * Show the validity state, in the configured mode.
70
+ *
71
+ * When it shows depends on what asked: a `change` (the candidate picked or dropped something)
72
+ * reports what they just did wrong, too many, at once; too few is not an error until they say they
73
+ * are done, so a `submit` (the item's end attempt, a form's submit) is what first shows it. After
74
+ * that first submit, changes report in full, so the message clears the moment the minimum is met
75
+ * and comes back if it is lost again. The validity itself is always set: a form with a
76
+ * below-minimum interaction cannot submit either way.
77
+ */
78
+ reportValidity(options?: {
79
+ trigger?: ValidityTrigger;
80
+ }): boolean;
57
81
  protected get validationDisplayMode(): ValidationDisplayMode;
58
82
  protected resolveAllowReorder(options?: {
59
83
  defaultWhenUnset?: boolean;
@@ -63,6 +87,7 @@ export declare abstract class Interaction extends LitElement implements Validata
63
87
  }): boolean;
64
88
  protected setInteractionValidity(isValid: boolean, validityMessage?: string, anchor?: HTMLElement | null, options?: {
65
89
  suppressInline?: boolean;
90
+ kind?: ValidityKind;
66
91
  }): void;
67
92
  protected updateInlineValidationMessage(): void;
68
93
  protected clearInlineValidationMessage(): void;
@@ -71,6 +96,12 @@ export declare abstract class Interaction extends LitElement implements Validata
71
96
  formResetCallback(): void;
72
97
  connectedCallback(): void;
73
98
  saveResponse(value: string | string[], state?: string | null): void;
99
+ /**
100
+ * `disabled` and `readonly` are states as well as attributes (qti-theme CONTRACT.md §3): a theme
101
+ * reads `:state(disabled)` on the host the way it reads it on a choice. The attribute stays
102
+ * reflected, so `[disabled]` keeps matching.
103
+ */
104
+ protected update(changed: PropertyValues): void;
74
105
  constructor();
75
106
  /**
76
107
  * Optional lifecycle bridge for interaction subclasses that do not need changed-property data.
@@ -10,6 +10,7 @@ import { property } from 'lit/decorators.js';
10
10
  import { configContext } from '../context/config.context';
11
11
  import { interactionContext } from '../context/interaction.context';
12
12
  import { itemContext } from '../context/item.context';
13
+ import { setState } from '../utils/state-aliases';
13
14
  /**
14
15
  * Shared QTI interaction base. Formative correction behavior is supplied by the
15
16
  * `@qti-components/corrections` package.
@@ -121,11 +122,43 @@ export class Interaction extends LitElement {
121
122
  set value(val) {
122
123
  this.response = val ? JSON.parse(val) : null;
123
124
  }
124
- reportValidity() {
125
+ /** Forget that a submit has reported: a reset or a new attempt waits for a submit again. */
126
+ resetValidityReport() {
127
+ this._validityReported = false;
128
+ this.clearInlineValidationMessage();
129
+ }
130
+ /**
131
+ * Show the validity state, in the configured mode.
132
+ *
133
+ * When it shows depends on what asked: a `change` (the candidate picked or dropped something)
134
+ * reports what they just did wrong, too many, at once; too few is not an error until they say they
135
+ * are done, so a `submit` (the item's end attempt, a form's submit) is what first shows it. After
136
+ * that first submit, changes report in full, so the message clears the moment the minimum is met
137
+ * and comes back if it is lost again. The validity itself is always set: a form with a
138
+ * below-minimum interaction cannot submit either way.
139
+ */
140
+ reportValidity(options) {
141
+ const trigger = options?.trigger ?? 'submit';
142
+ // A submit judges the response as it is now, whatever changed it (a restore, a host write).
143
+ if (trigger === 'submit')
144
+ this.validate?.();
125
145
  const isValid = this._internals.validity.valid;
126
146
  const mode = this.validationDisplayMode;
147
+ if (trigger === 'submit')
148
+ this._validityReported = true;
149
+ if (trigger === 'change' && !this._validityReported && this._validityKind === 'min') {
150
+ this.clearInlineValidationMessage();
151
+ return isValid;
152
+ }
127
153
  if (mode === 'native' || mode === 'both') {
128
- this._internals.reportValidity();
154
+ // `internals.reportValidity()` fires `invalid` on this host; that one is ours, not a submit.
155
+ this._reportingNatively = true;
156
+ try {
157
+ this._internals.reportValidity();
158
+ }
159
+ finally {
160
+ this._reportingNatively = false;
161
+ }
129
162
  }
130
163
  if (mode === 'inline' || mode === 'both') {
131
164
  this.updateInlineValidationMessage();
@@ -173,6 +206,7 @@ export class Interaction extends LitElement {
173
206
  setInteractionValidity(isValid, validityMessage = '', anchor, options) {
174
207
  const validityAnchor = anchor ?? this;
175
208
  this._internals.setValidity(isValid ? {} : { customError: true }, validityMessage, validityAnchor);
209
+ this._validityKind = isValid ? undefined : (options?.kind ?? 'other');
176
210
  if (options?.suppressInline !== true &&
177
211
  (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both')) {
178
212
  this.updateInlineValidationMessage();
@@ -203,9 +237,11 @@ export class Interaction extends LitElement {
203
237
  return this.shadowRoot?.querySelector('#validation-message');
204
238
  }
205
239
  reset() {
240
+ this.resetValidityReport();
206
241
  this.response = null;
207
242
  }
208
243
  formResetCallback() {
244
+ this.resetValidityReport();
209
245
  this.reset();
210
246
  }
211
247
  connectedCallback() {
@@ -235,6 +271,30 @@ export class Interaction extends LitElement {
235
271
  }
236
272
  }));
237
273
  }
274
+ /**
275
+ * `disabled` and `readonly` are states as well as attributes (qti-theme CONTRACT.md §3): a theme
276
+ * reads `:state(disabled)` on the host the way it reads it on a choice. The attribute stays
277
+ * reflected, so `[disabled]` keeps matching.
278
+ */
279
+ update(changed) {
280
+ const first = !this.hasUpdated;
281
+ super.update(changed);
282
+ // Validity from the start, not only from the candidate's first change: an untouched interaction
283
+ // below its minimum is invalid, so a form holding it does not submit. Silent: a validate never
284
+ // shows a message, a report does. Twice and no more: once after the first render, and once two
285
+ // frames later, when a restored response has been applied (choices' checked states and placed
286
+ // chips land in the updates after the first). A submit validates again (reportValidity).
287
+ // Bounded on purpose: a validate can cause an update, so validating per update loops.
288
+ if (first) {
289
+ const run = () => this.isConnected && this.validate();
290
+ void this.updateComplete.then(run);
291
+ requestAnimationFrame(() => requestAnimationFrame(run));
292
+ }
293
+ if (changed.has('disabled'))
294
+ setState(this._internals.states, 'disabled', this.disabled);
295
+ if (changed.has('readonly'))
296
+ setState(this._internals.states, 'readonly', this.readonly);
297
+ }
238
298
  constructor() {
239
299
  super();
240
300
  /**
@@ -261,7 +321,24 @@ export class Interaction extends LitElement {
261
321
  this._interactionContext = { choiceRole: null, draggablesSelector: null };
262
322
  this.disabled = false;
263
323
  this.readonly = false;
324
+ /** Set once a submit has reported; from then on every change reports in full. */
325
+ this._validityReported = false;
326
+ /** True while this element asks its own internals to report, so its `invalid` is not a submit. */
327
+ this._reportingNatively = false;
264
328
  this._internals = this.attachInternals();
329
+ // A form's submit runs the browser's own validation, which fires `invalid` on an invalid
330
+ // element and shows the native message at its anchor. That is a submit: show the inline
331
+ // message too, and from now on report every change in full. Ignored while this element
332
+ // reports through its own internals (that fires `invalid` too). A host calling
333
+ // `form.checkValidity()` also lands here and cannot be told apart from a submit; it counts as one.
334
+ this.addEventListener('invalid', () => {
335
+ if (this._reportingNatively)
336
+ return;
337
+ this._validityReported = true;
338
+ if (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both') {
339
+ this.updateInlineValidationMessage();
340
+ }
341
+ });
265
342
  this.addEventListener('qti-interaction-response', (e) => {
266
343
  this.#lastResponse = JSON.stringify(e.detail?.response ?? null);
267
344
  });
@@ -1 +1 @@
1
- {"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/abstract/interaction.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAStD;;;GAGG;AACH,MAAM,OAAgB,WAAY,SAAQ,UAAU;IAClD;;;;;OAKG;IACH,YAAY,CAIT;IAEH,IAAY,QAAQ;QAClB,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;IACjC,CAAC;IAED,aAAa,CAAW;IACxB,aAAa,CAAW;IAExB;;;;OAIG;IACH,WAAW,CAAC,OAAgC;QAC1C,IAAI,OAAO,EAAE,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;YAC/E,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC;YACtC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACnC,CAAC;QACD,IAAI,OAAO,EAAE,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;YAC/E,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC;YACtC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,sGAAsG;IACtG,aAAa,CAAU;IACvB;;;;;OAKG;IACH,aAAa,CAAU;IACvB,eAAe,CAAS;IAExB;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,OAAgC;QAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,kBAAkB,IAAI,IAAI,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC;QAC/E,MAAM,QAAQ,GAAG,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC;QAC7F,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;gBAC1B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;gBAC5B,KAAK,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE;oBACjC,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;oBAC7B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACrC,CAAC,CAAC,CAAC;YACL,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,KAAK,SAAS,CAAC;QAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;QAC1D,IAAI,UAAU,KAAK,IAAI,CAAC,aAAa;YAAE,OAAO;QAC9C,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC;QAChC,IAAI,KAAK,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC;YAAE,OAAO;QAC/E,IAAI,UAAU,KAAK,IAAI,CAAC,aAAa;YAAE,OAAO;QAC9C,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC;QAChC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,KAAiC,CAAC;IAC7D,CAAC;IA6BD,iDAAiD,CAAS;aAanD,mBAAc,GAAG,IAAI,AAAP,CAAQ;IAG7B,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAUD,qFAAqF;IACrF,IAAc,iBAAiB;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,gBAAgB;QAClB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC;YAC9B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,iBAAiB,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAuB,CAAC;QAC3G,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC;QACpE,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,kBAAkB,CAAC,CAAC;IAC1E,CAAC;IAID,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,KAAK,CAAC,GAAkB;QAC1B,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAKM,cAAc;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,CAAC;QAExC,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC;QACnC,CAAC;QAED,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACzC,IAAI,CAAC,6BAA6B,EAAE,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,4BAA4B,EAAE,CAAC;QACtC,CAAC;QAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,4BAA4B,EAAE,CAAC;QACtC,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAc,qBAAqB;QACjC,OAAO,IAAI,CAAC,aAAa,EAAE,qBAAqB,IAAI,QAAQ,CAAC;IAC/D,CAAC;IAES,mBAAmB,CAAC,OAAwC;QACpE,MAAM,gBAAgB,GAAG,OAAO,EAAE,gBAAgB,IAAI,IAAI,CAAC;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,gBAAgB,CAAC;QAC1B,CAAC;QACD,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACtC,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,CAAC;QACD,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAES,6BAA6B,CAAC,OAAwC;QAC9E,MAAM,gBAAgB,GAAG,OAAO,EAAE,gBAAgB,IAAI,KAAK,CAAC;QAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC;QAElC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,gBAAgB,CAAC;QAC1B,CAAC;QAED,IAAI,MAAM,CAAC,sBAAsB,KAAK,SAAS,EAAE,CAAC;YAChD,OAAO,MAAM,CAAC,sBAAsB,CAAC;QACvC,CAAC;QAED,IAAI,MAAM,CAAC,+BAA+B,KAAK,SAAS,EAAE,CAAC;YACzD,IAAI,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC;gBAC5D,IAAI,CAAC,iDAAiD,GAAG,IAAI,CAAC;gBAC9D,OAAO,CAAC,IAAI,CACV,qGAAqG,CACtG,CAAC;YACJ,CAAC;YACD,OAAO,MAAM,CAAC,+BAA+B,CAAC;QAChD,CAAC;QAED,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAES,sBAAsB,CAC9B,OAAgB,EAChB,eAAe,GAAG,EAAE,EACpB,MAA2B,EAC3B,OAAsC;QAEtC,MAAM,cAAc,GAAG,MAAM,IAAI,IAAI,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,cAAc,CAAC,CAAC;QAEnG,IACE,OAAO,EAAE,cAAc,KAAK,IAAI;YAChC,CAAC,IAAI,CAAC,qBAAqB,KAAK,QAAQ,IAAI,IAAI,CAAC,qBAAqB,KAAK,MAAM,CAAC,EAClF,CAAC;YACD,IAAI,CAAC,6BAA6B,EAAE,CAAC;QACvC,CAAC;IACH,CAAC;IAES,6BAA6B;QACrC,MAAM,wBAAwB,GAAG,IAAI,CAAC,2BAA2B,EAAE,CAAC;QACpE,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpC,wBAAwB,CAAC,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACzE,wBAAwB,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;YAC5E,OAAO;QACT,CAAC;QAED,wBAAwB,CAAC,WAAW,GAAG,EAAE,CAAC;QAC1C,wBAAwB,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAClD,CAAC;IAES,4BAA4B;QACpC,MAAM,wBAAwB,GAAG,IAAI,CAAC,2BAA2B,EAAE,CAAC;QACpE,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,wBAAwB,CAAC,WAAW,GAAG,EAAE,CAAC;QAC1C,wBAAwB,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAClD,CAAC;IAES,2BAA2B;QACnC,OAAO,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,qBAAqB,CAAuB,CAAC;IACrF,CAAC;IAEM,KAAK;QACV,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;IAEM,iBAAiB;QACtB,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAEe,iBAAiB;QAC/B,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAE1B,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,0BAA0B,EAAE;YAC1C,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,KAAK;YACjB,MAAM,EAAE;gBACN,kBAAkB,EAAE,IAAI;gBACxB,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;aAC5C;SACF,CAAC,CACH,CAAC;IACJ,CAAC;IAEM,YAAY,CAAC,KAAwB,EAAE,KAAqB;QACjE,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,0BAA0B,EAAE;YAC1C,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,KAAK;YACjB,MAAM,EAAE;gBACN,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;gBAC3C,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK;gBACnD,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1C;SACF,CAAC,CACH,CAAC;IACJ,CAAC;IAED;QACE,KAAK,EAAE,CAAC;QAvUV;;;;;WAKG;QACH,iBAAY,GAAG,IAAI,eAAe,CAAC,IAAI,EAAE;YACvC,OAAO,EAAE,WAAW;YACpB,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC;SAC/C,CAAC,CAAC;QAmCH,oBAAe,GAAG,KAAK,CAAC;QAsExB,sDAAiD,GAAG,KAAK,CAAC;QAE1D;;;;;;;WAOG;QAEO,wBAAmB,GAAiC,EAAE,UAAU,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC;QAWjE,aAAQ,GAAG,KAAK,CAAC;QAEjB,aAAQ,GAAG,KAAK,CAAC;QA6L3D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACzC,IAAI,CAAC,gBAAgB,CAAC,0BAA0B,EAAE,CAAC,CAAQ,EAAE,EAAE;YAC7D,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,SAAS,CAAE,CAAiB,CAAC,MAAM,EAAE,QAAQ,IAAI,IAAI,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;OAWG;IACgB,YAAY,CAAC,kBAAmC,IAAS,CAAC;;AAzOtE;IADN,OAAO,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;kDACjB;AAa1B;IADT,OAAO,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;wDACoE;AAS/C;IAA7D,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,qBAAqB,EAAE,CAAC;uDAA4B;AAE7C;IAA3C,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;6CAAkB;AAEjB;IAA3C,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;6CAAkB;AAEjC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;yCAAM","sourcesContent":["import { ContextConsumer, consume, provide } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport { configContext } from '../context/config.context';\nimport { interactionContext } from '../context/interaction.context';\nimport { itemContext } from '../context/item.context';\n\nimport type { PropertyValues } from 'lit';\nimport type { InteractionContext } from '../context/interaction.context';\nimport type { ConfigContext, ValidationDisplayMode } from '../context/config.context';\nimport type { ItemContext } from '../context/types/item.types';\nimport type { ValidatableInteraction } from '../lib/interaction.interface';\nimport type { ResponseVariable } from '../lib/variables';\n\n/**\n * Shared QTI interaction base. Formative correction behavior is supplied by the\n * `@qti-components/corrections` package.\n */\nexport abstract class Interaction extends LitElement implements ValidatableInteraction {\n /**\n * The item's variables. The interaction takes its response from here as well as publishing it:\n * when the item's value for this response changes to something other than what the interaction\n * shows, it adopts it. That is how `item.variables = …` reaches it, with no list of interactions\n * kept by the item. See {@link Interaction.#adoptResponse}.\n */\n #itemContext = new ContextConsumer(this, {\n context: itemContext,\n subscribe: true,\n callback: context => this.#followItem(context)\n });\n\n private get _context(): ItemContext | undefined {\n return this.#itemContext.value;\n }\n\n #seenReadonly?: boolean;\n #seenDisabled?: boolean;\n\n /**\n * Follows the item: its `readonly` and `disabled`, and the value it holds for this response.\n * `readonly` and `disabled` are only taken when the item says something, so an interaction\n * authored with either stays so inside an item that does not mention it.\n */\n #followItem(context: ItemContext | undefined) {\n if (context?.readonly !== undefined && context.readonly !== this.#seenReadonly) {\n this.#seenReadonly = context.readonly;\n this.readonly = context.readonly;\n }\n if (context?.disabled !== undefined && context.disabled !== this.#seenDisabled) {\n this.#seenDisabled = context.disabled;\n this.disabled = context.disabled;\n }\n this.#adoptResponse(context);\n }\n\n /** The item's last value for this response, serialised, to tell a change from an unrelated update. */\n #seenResponse?: string;\n /**\n * What this interaction last published or adopted, serialised. It is kept from the interaction's\n * own `qti-interaction-response` events, which every emitter dispatches on the interaction itself,\n * and not read back from `response`: that getter has no common shape (the drag-drop interactions\n * return one comma-joined string where they publish an array), so it cannot be compared.\n */\n #lastResponse?: string;\n #adoptScheduled = false;\n\n /**\n * Adopts the item's value for this response when it changed and the interaction does not already\n * show it.\n *\n * Two things keep this from fighting the candidate. It reacts to the item's value for *this*\n * response changing, not to any context update, so a score or a timer ticking never rewrites what\n * is being typed. And when the item's value is what the interaction itself last published (which\n * is what happens right after it publishes an answer) it does nothing, so there is no echo.\n *\n * The first value is only adopted when it says something: an unanswered item must not wipe a\n * `response` attribute the author put on the interaction.\n *\n * Setting `response` on an interaction that has not rendered yet is what the item used to avoid\n * by waiting, so until the first update this waits too, then looks at the latest value.\n */\n #adoptResponse(context: ItemContext | undefined) {\n const id = this.responseIdentifier ?? this.getAttribute('response-identifier');\n const variable = context?.variables?.find(v => v.type === 'response' && v.identifier === id);\n if (!variable) return;\n\n if (!this.hasUpdated) {\n if (!this.#adoptScheduled) {\n this.#adoptScheduled = true;\n void this.updateComplete.then(() => {\n this.#adoptScheduled = false;\n this.#adoptResponse(this._context);\n });\n }\n return;\n }\n\n const first = this.#seenResponse === undefined;\n const serialised = JSON.stringify(variable.value ?? null);\n if (serialised === this.#seenResponse) return;\n this.#seenResponse = serialised;\n if (first && (variable.value === null || variable.value === undefined)) return;\n if (serialised === this.#lastResponse) return;\n this.#lastResponse = serialised;\n this.response = variable.value as string | string[] | null;\n }\n\n /**\n * Delivery configuration, from the nearest provider — `qti-test` or `qti-item`.\n *\n * **Public so it can be assigned directly, which is the development-time route.** An interaction\n * has to work standalone, and in a story or a spec there is usually no provider above it — so\n * setting the field is all that is needed:\n *\n * orderInteraction.configContext = { allowReorder: false, validationDisplayMode: 'none' };\n *\n * With no provider, nothing ever overwrites it. That is the whole contract, and its limit: where a\n * provider DOES exist it re-emits on its next update and wins, because `@consume` writes this same\n * field. There is no merging — set the value on the provider in that case.\n *\n * In a lit-html template, bind it as a property rather than assigning after the fact:\n *\n * html`<qti-order-interaction .configContext=${{ allowReorder: false }}>…`\n *\n * The binding is committed on the detached template clone, before the fragment is inserted, so it\n * is in place by `connectedCallback` — which matters for anything that reads config on connect.\n *\n * It was `protected`, which fooled nobody: every caller reached it anyway through an\n * `as any` / `as { configContext?: … }` cast. Public says what was already true and removes the\n * casts, rather than pretending at an encapsulation the type system was the only thing enforcing.\n */\n @consume({ context: configContext, subscribe: true })\n public configContext: ConfigContext;\n\n #didLogDisableAfterIfMaxChoicesReachedDeprecation = false;\n\n /*\n * Published to the choice elements inside this interaction — their role, and how to tell\n * whether they are draggable. Provided here on the base rather than in a mixin so that\n * ChoicesMixin and the drag-drop mixins can each contribute without two providers of the same\n * context competing on one element.\n *\n * Reassign, never mutate: an in-place change does not notify consumers.\n */\n @provide({ context: interactionContext })\n protected _interactionContext: Readonly<InteractionContext> = { choiceRole: null, draggablesSelector: null };\n\n static formAssociated = true;\n protected _internals: ElementInternals;\n\n get internals(): ElementInternals {\n return this._internals;\n }\n\n @property({ type: String, attribute: 'response-identifier' }) responseIdentifier: string;\n\n @property({ reflect: true, type: Boolean }) disabled = false;\n\n @property({ reflect: true, type: Boolean }) readonly = false;\n\n @property({ type: String }) name;\n\n /** Extension point used by wrappers that should not register as candidate inputs. */\n protected get registersWithItem(): boolean {\n return true;\n }\n\n get isInline(): boolean {\n return false;\n }\n\n get responseVariable(): ResponseVariable | undefined {\n if (!this._context?.variables) {\n return undefined;\n }\n const responseVariables = this._context.variables.filter(v => v.type === 'response') as ResponseVariable[];\n const responseIdentifier = this.getAttribute('response-identifier');\n return responseVariables.find(v => v.identifier === responseIdentifier);\n }\n\n abstract validate(): boolean;\n\n get value(): string | null {\n return JSON.stringify(this.response);\n }\n\n set value(val: string | null) {\n this.response = val ? JSON.parse(val) : null;\n }\n\n abstract get response(): string | string[] | null;\n abstract set response(val: string | string[] | null);\n\n public reportValidity(): boolean {\n const isValid = this._internals.validity.valid;\n const mode = this.validationDisplayMode;\n\n if (mode === 'native' || mode === 'both') {\n this._internals.reportValidity();\n }\n\n if (mode === 'inline' || mode === 'both') {\n this.updateInlineValidationMessage();\n } else {\n this.clearInlineValidationMessage();\n }\n\n if (mode === 'none') {\n this.clearInlineValidationMessage();\n }\n\n return isValid;\n }\n\n protected get validationDisplayMode(): ValidationDisplayMode {\n return this.configContext?.validationDisplayMode ?? 'inline';\n }\n\n protected resolveAllowReorder(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? true;\n const config = this.configContext;\n if (!config) {\n return defaultWhenUnset;\n }\n if (config.allowReorder !== undefined) {\n return config.allowReorder;\n }\n return defaultWhenUnset;\n }\n\n protected resolveDisableAfterMaxReached(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? false;\n const config = this.configContext;\n\n if (!config) {\n return defaultWhenUnset;\n }\n\n if (config.disableAfterMaxReached !== undefined) {\n return config.disableAfterMaxReached;\n }\n\n if (config.disableAfterIfMaxChoicesReached !== undefined) {\n if (!this.#didLogDisableAfterIfMaxChoicesReachedDeprecation) {\n this.#didLogDisableAfterIfMaxChoicesReachedDeprecation = true;\n console.warn(\n '[QTI Config] `disableAfterIfMaxChoicesReached` is deprecated. Use `disableAfterMaxReached` instead.'\n );\n }\n return config.disableAfterIfMaxChoicesReached;\n }\n\n return defaultWhenUnset;\n }\n\n protected setInteractionValidity(\n isValid: boolean,\n validityMessage = '',\n anchor?: HTMLElement | null,\n options?: { suppressInline?: boolean }\n ): void {\n const validityAnchor = anchor ?? this;\n this._internals.setValidity(isValid ? {} : { customError: true }, validityMessage, validityAnchor);\n\n if (\n options?.suppressInline !== true &&\n (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both')\n ) {\n this.updateInlineValidationMessage();\n }\n }\n\n protected updateInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n if (!this._internals.validity.valid) {\n validationMessageElement.textContent = this._internals.validationMessage;\n validationMessageElement.style.setProperty('display', 'block', 'important');\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected clearInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected getValidationMessageElement(): HTMLElement | null {\n return this.shadowRoot?.querySelector('#validation-message') as HTMLElement | null;\n }\n\n public reset(): void {\n this.response = null;\n }\n\n public formResetCallback(): void {\n this.reset();\n }\n\n public override connectedCallback(): void {\n super.connectedCallback();\n\n if (!this.registersWithItem) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent('qti-register-interaction', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n interactionElement: this,\n responseIdentifier: this.responseIdentifier\n }\n })\n );\n }\n\n public saveResponse(value: string | string[], state?: string | null): void {\n this.dispatchEvent(\n new CustomEvent('qti-interaction-response', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n responseIdentifier: this.responseIdentifier,\n response: Array.isArray(value) ? [...value] : value,\n ...(state !== undefined ? { state } : {})\n }\n })\n );\n }\n\n constructor() {\n super();\n this._internals = this.attachInternals();\n this.addEventListener('qti-interaction-response', (e: Event) => {\n this.#lastResponse = JSON.stringify((e as CustomEvent).detail?.response ?? null);\n });\n }\n\n /**\n * Optional lifecycle bridge for interaction subclasses that do not need changed-property data.\n *\n * The parameter is `PropertyValues`, matching LitElement, and NOT `PropertyValues<this>`.\n * The polymorphic `this` narrowed the parameter per subclass, which made every interaction\n * class structurally incompatible with `LitElement` — so `Constructor<LitElement>`, the\n * standard constraint for a Lit mixin, rejected all of them and consumers could not wrap an\n * interaction in a mixin without casting. `this` buys nothing in a parameter position and\n * costs contravariant compatibility with the base class.\n *\n * Still optional, so subclasses that ignore the argument can override with `firstUpdated()`.\n */\n protected override firstUpdated(_changedProperties?: PropertyValues): void {}\n}\n"]}
1
+ {"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/abstract/interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextConsumer, consume, provide } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport { configContext } from '../context/config.context';\nimport { interactionContext } from '../context/interaction.context';\nimport { itemContext } from '../context/item.context';\nimport { setState } from '../utils/state-aliases';\n\nimport type { PropertyValues } from 'lit';\nimport type { InteractionContext } from '../context/interaction.context';\nimport type { ConfigContext, ValidationDisplayMode } from '../context/config.context';\nimport type { ItemContext } from '../context/types/item.types';\nimport type { ValidatableInteraction } from '../lib/interaction.interface';\nimport type { ResponseVariable } from '../lib/variables';\n\n/** Why an interaction is invalid: too few (`min`), too many (`max`), or something else (`other`). */\nexport type ValidityKind = 'min' | 'max' | 'other';\n/** What asked for the report: the candidate changing the response, or a submit (end attempt, form). */\nexport type ValidityTrigger = 'change' | 'submit';\n\n/**\n * Shared QTI interaction base. Formative correction behavior is supplied by the\n * `@qti-components/corrections` package.\n */\nexport abstract class Interaction extends LitElement implements ValidatableInteraction {\n /**\n * The item's variables. The interaction takes its response from here as well as publishing it:\n * when the item's value for this response changes to something other than what the interaction\n * shows, it adopts it. That is how `item.variables = …` reaches it, with no list of interactions\n * kept by the item. See {@link Interaction.#adoptResponse}.\n */\n #itemContext = new ContextConsumer(this, {\n context: itemContext,\n subscribe: true,\n callback: context => this.#followItem(context)\n });\n\n private get _context(): ItemContext | undefined {\n return this.#itemContext.value;\n }\n\n #seenReadonly?: boolean;\n #seenDisabled?: boolean;\n\n /**\n * Follows the item: its `readonly` and `disabled`, and the value it holds for this response.\n * `readonly` and `disabled` are only taken when the item says something, so an interaction\n * authored with either stays so inside an item that does not mention it.\n */\n #followItem(context: ItemContext | undefined) {\n if (context?.readonly !== undefined && context.readonly !== this.#seenReadonly) {\n this.#seenReadonly = context.readonly;\n this.readonly = context.readonly;\n }\n if (context?.disabled !== undefined && context.disabled !== this.#seenDisabled) {\n this.#seenDisabled = context.disabled;\n this.disabled = context.disabled;\n }\n this.#adoptResponse(context);\n }\n\n /** The item's last value for this response, serialised, to tell a change from an unrelated update. */\n #seenResponse?: string;\n /**\n * What this interaction last published or adopted, serialised. It is kept from the interaction's\n * own `qti-interaction-response` events, which every emitter dispatches on the interaction itself,\n * and not read back from `response`: that getter has no common shape (the drag-drop interactions\n * return one comma-joined string where they publish an array), so it cannot be compared.\n */\n #lastResponse?: string;\n #adoptScheduled = false;\n\n /**\n * Adopts the item's value for this response when it changed and the interaction does not already\n * show it.\n *\n * Two things keep this from fighting the candidate. It reacts to the item's value for *this*\n * response changing, not to any context update, so a score or a timer ticking never rewrites what\n * is being typed. And when the item's value is what the interaction itself last published (which\n * is what happens right after it publishes an answer) it does nothing, so there is no echo.\n *\n * The first value is only adopted when it says something: an unanswered item must not wipe a\n * `response` attribute the author put on the interaction.\n *\n * Setting `response` on an interaction that has not rendered yet is what the item used to avoid\n * by waiting, so until the first update this waits too, then looks at the latest value.\n */\n #adoptResponse(context: ItemContext | undefined) {\n const id = this.responseIdentifier ?? this.getAttribute('response-identifier');\n const variable = context?.variables?.find(v => v.type === 'response' && v.identifier === id);\n if (!variable) return;\n\n if (!this.hasUpdated) {\n if (!this.#adoptScheduled) {\n this.#adoptScheduled = true;\n void this.updateComplete.then(() => {\n this.#adoptScheduled = false;\n this.#adoptResponse(this._context);\n });\n }\n return;\n }\n\n const first = this.#seenResponse === undefined;\n const serialised = JSON.stringify(variable.value ?? null);\n if (serialised === this.#seenResponse) return;\n this.#seenResponse = serialised;\n if (first && (variable.value === null || variable.value === undefined)) return;\n if (serialised === this.#lastResponse) return;\n this.#lastResponse = serialised;\n this.response = variable.value as string | string[] | null;\n }\n\n /**\n * Delivery configuration, from the nearest provider — `qti-test` or `qti-item`.\n *\n * **Public so it can be assigned directly, which is the development-time route.** An interaction\n * has to work standalone, and in a story or a spec there is usually no provider above it — so\n * setting the field is all that is needed:\n *\n * orderInteraction.configContext = { allowReorder: false, validationDisplayMode: 'none' };\n *\n * With no provider, nothing ever overwrites it. That is the whole contract, and its limit: where a\n * provider DOES exist it re-emits on its next update and wins, because `@consume` writes this same\n * field. There is no merging — set the value on the provider in that case.\n *\n * In a lit-html template, bind it as a property rather than assigning after the fact:\n *\n * html`<qti-order-interaction .configContext=${{ allowReorder: false }}>…`\n *\n * The binding is committed on the detached template clone, before the fragment is inserted, so it\n * is in place by `connectedCallback` — which matters for anything that reads config on connect.\n *\n * It was `protected`, which fooled nobody: every caller reached it anyway through an\n * `as any` / `as { configContext?: … }` cast. Public says what was already true and removes the\n * casts, rather than pretending at an encapsulation the type system was the only thing enforcing.\n */\n @consume({ context: configContext, subscribe: true })\n public configContext: ConfigContext;\n\n #didLogDisableAfterIfMaxChoicesReachedDeprecation = false;\n\n /*\n * Published to the choice elements inside this interaction — their role, and how to tell\n * whether they are draggable. Provided here on the base rather than in a mixin so that\n * ChoicesMixin and the drag-drop mixins can each contribute without two providers of the same\n * context competing on one element.\n *\n * Reassign, never mutate: an in-place change does not notify consumers.\n */\n @provide({ context: interactionContext })\n protected _interactionContext: Readonly<InteractionContext> = { choiceRole: null, draggablesSelector: null };\n\n static formAssociated = true;\n protected _internals: ElementInternals;\n\n get internals(): ElementInternals {\n return this._internals;\n }\n\n @property({ type: String, attribute: 'response-identifier' }) responseIdentifier: string;\n\n @property({ reflect: true, type: Boolean }) disabled = false;\n\n @property({ reflect: true, type: Boolean }) readonly = false;\n\n @property({ type: String }) name;\n\n /** Extension point used by wrappers that should not register as candidate inputs. */\n protected get registersWithItem(): boolean {\n return true;\n }\n\n get isInline(): boolean {\n return false;\n }\n\n get responseVariable(): ResponseVariable | undefined {\n if (!this._context?.variables) {\n return undefined;\n }\n const responseVariables = this._context.variables.filter(v => v.type === 'response') as ResponseVariable[];\n const responseIdentifier = this.getAttribute('response-identifier');\n return responseVariables.find(v => v.identifier === responseIdentifier);\n }\n\n abstract validate(): boolean;\n\n get value(): string | null {\n return JSON.stringify(this.response);\n }\n\n set value(val: string | null) {\n this.response = val ? JSON.parse(val) : null;\n }\n\n abstract get response(): string | string[] | null;\n abstract set response(val: string | string[] | null);\n\n /** Why the interaction is invalid right now; `undefined` while it is valid. */\n protected _validityKind?: ValidityKind;\n /** Set once a submit has reported; from then on every change reports in full. */\n protected _validityReported = false;\n /** True while this element asks its own internals to report, so its `invalid` is not a submit. */\n protected _reportingNatively = false;\n\n /** Forget that a submit has reported: a reset or a new attempt waits for a submit again. */\n protected resetValidityReport(): void {\n this._validityReported = false;\n this.clearInlineValidationMessage();\n }\n\n /**\n * Show the validity state, in the configured mode.\n *\n * When it shows depends on what asked: a `change` (the candidate picked or dropped something)\n * reports what they just did wrong, too many, at once; too few is not an error until they say they\n * are done, so a `submit` (the item's end attempt, a form's submit) is what first shows it. After\n * that first submit, changes report in full, so the message clears the moment the minimum is met\n * and comes back if it is lost again. The validity itself is always set: a form with a\n * below-minimum interaction cannot submit either way.\n */\n public reportValidity(options?: { trigger?: ValidityTrigger }): boolean {\n const trigger = options?.trigger ?? 'submit';\n // A submit judges the response as it is now, whatever changed it (a restore, a host write).\n if (trigger === 'submit') this.validate?.();\n const isValid = this._internals.validity.valid;\n const mode = this.validationDisplayMode;\n\n if (trigger === 'submit') this._validityReported = true;\n if (trigger === 'change' && !this._validityReported && this._validityKind === 'min') {\n this.clearInlineValidationMessage();\n return isValid;\n }\n\n if (mode === 'native' || mode === 'both') {\n // `internals.reportValidity()` fires `invalid` on this host; that one is ours, not a submit.\n this._reportingNatively = true;\n try {\n this._internals.reportValidity();\n } finally {\n this._reportingNatively = false;\n }\n }\n\n if (mode === 'inline' || mode === 'both') {\n this.updateInlineValidationMessage();\n } else {\n this.clearInlineValidationMessage();\n }\n\n if (mode === 'none') {\n this.clearInlineValidationMessage();\n }\n\n return isValid;\n }\n\n protected get validationDisplayMode(): ValidationDisplayMode {\n return this.configContext?.validationDisplayMode ?? 'inline';\n }\n\n protected resolveAllowReorder(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? true;\n const config = this.configContext;\n if (!config) {\n return defaultWhenUnset;\n }\n if (config.allowReorder !== undefined) {\n return config.allowReorder;\n }\n return defaultWhenUnset;\n }\n\n protected resolveDisableAfterMaxReached(options?: { defaultWhenUnset?: boolean }): boolean {\n const defaultWhenUnset = options?.defaultWhenUnset ?? false;\n const config = this.configContext;\n\n if (!config) {\n return defaultWhenUnset;\n }\n\n if (config.disableAfterMaxReached !== undefined) {\n return config.disableAfterMaxReached;\n }\n\n if (config.disableAfterIfMaxChoicesReached !== undefined) {\n if (!this.#didLogDisableAfterIfMaxChoicesReachedDeprecation) {\n this.#didLogDisableAfterIfMaxChoicesReachedDeprecation = true;\n console.warn(\n '[QTI Config] `disableAfterIfMaxChoicesReached` is deprecated. Use `disableAfterMaxReached` instead.'\n );\n }\n return config.disableAfterIfMaxChoicesReached;\n }\n\n return defaultWhenUnset;\n }\n\n protected setInteractionValidity(\n isValid: boolean,\n validityMessage = '',\n anchor?: HTMLElement | null,\n options?: { suppressInline?: boolean; kind?: ValidityKind }\n ): void {\n const validityAnchor = anchor ?? this;\n this._internals.setValidity(isValid ? {} : { customError: true }, validityMessage, validityAnchor);\n this._validityKind = isValid ? undefined : (options?.kind ?? 'other');\n\n if (\n options?.suppressInline !== true &&\n (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both')\n ) {\n this.updateInlineValidationMessage();\n }\n }\n\n protected updateInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n if (!this._internals.validity.valid) {\n validationMessageElement.textContent = this._internals.validationMessage;\n validationMessageElement.style.setProperty('display', 'block', 'important');\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected clearInlineValidationMessage(): void {\n const validationMessageElement = this.getValidationMessageElement();\n if (!validationMessageElement) {\n return;\n }\n\n validationMessageElement.textContent = '';\n validationMessageElement.style.display = 'none';\n }\n\n protected getValidationMessageElement(): HTMLElement | null {\n return this.shadowRoot?.querySelector('#validation-message') as HTMLElement | null;\n }\n\n public reset(): void {\n this.resetValidityReport();\n this.response = null;\n }\n\n public formResetCallback(): void {\n this.resetValidityReport();\n this.reset();\n }\n\n public override connectedCallback(): void {\n super.connectedCallback();\n\n if (!this.registersWithItem) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent('qti-register-interaction', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n interactionElement: this,\n responseIdentifier: this.responseIdentifier\n }\n })\n );\n }\n\n public saveResponse(value: string | string[], state?: string | null): void {\n this.dispatchEvent(\n new CustomEvent('qti-interaction-response', {\n bubbles: true,\n composed: true,\n cancelable: false,\n detail: {\n responseIdentifier: this.responseIdentifier,\n response: Array.isArray(value) ? [...value] : value,\n ...(state !== undefined ? { state } : {})\n }\n })\n );\n }\n\n /**\n * `disabled` and `readonly` are states as well as attributes (qti-theme CONTRACT.md §3): a theme\n * reads `:state(disabled)` on the host the way it reads it on a choice. The attribute stays\n * reflected, so `[disabled]` keeps matching.\n */\n protected override update(changed: PropertyValues): void {\n const first = !this.hasUpdated;\n super.update(changed);\n // Validity from the start, not only from the candidate's first change: an untouched interaction\n // below its minimum is invalid, so a form holding it does not submit. Silent: a validate never\n // shows a message, a report does. Twice and no more: once after the first render, and once two\n // frames later, when a restored response has been applied (choices' checked states and placed\n // chips land in the updates after the first). A submit validates again (reportValidity).\n // Bounded on purpose: a validate can cause an update, so validating per update loops.\n if (first) {\n const run = () => this.isConnected && this.validate();\n void this.updateComplete.then(run);\n requestAnimationFrame(() => requestAnimationFrame(run));\n }\n if (changed.has('disabled')) setState(this._internals.states, 'disabled', this.disabled);\n if (changed.has('readonly')) setState(this._internals.states, 'readonly', this.readonly);\n }\n\n constructor() {\n super();\n this._internals = this.attachInternals();\n // A form's submit runs the browser's own validation, which fires `invalid` on an invalid\n // element and shows the native message at its anchor. That is a submit: show the inline\n // message too, and from now on report every change in full. Ignored while this element\n // reports through its own internals (that fires `invalid` too). A host calling\n // `form.checkValidity()` also lands here and cannot be told apart from a submit; it counts as one.\n this.addEventListener('invalid', () => {\n if (this._reportingNatively) return;\n this._validityReported = true;\n if (this.validationDisplayMode === 'inline' || this.validationDisplayMode === 'both') {\n this.updateInlineValidationMessage();\n }\n });\n this.addEventListener('qti-interaction-response', (e: Event) => {\n this.#lastResponse = JSON.stringify((e as CustomEvent).detail?.response ?? null);\n });\n }\n\n /**\n * Optional lifecycle bridge for interaction subclasses that do not need changed-property data.\n *\n * The parameter is `PropertyValues`, matching LitElement, and NOT `PropertyValues<this>`.\n * The polymorphic `this` narrowed the parameter per subclass, which made every interaction\n * class structurally incompatible with `LitElement` — so `Constructor<LitElement>`, the\n * standard constraint for a Lit mixin, rejected all of them and consumers could not wrap an\n * interaction in a mixin without casting. `this` buys nothing in a parameter position and\n * costs contravariant compatibility with the base class.\n *\n * Still optional, so subclasses that ignore the argument can override with `firstUpdated()`.\n */\n protected override firstUpdated(_changedProperties?: PropertyValues): void {}\n}\n"]}
@@ -10,7 +10,7 @@ export declare abstract class QtiFeedback extends LitElement {
10
10
  checkShowFeedback(outcomeIdentifier: string): void;
11
11
  /**
12
12
  * Called when this feedback is about to show. Return `false` to keep it hidden: the host shows the
13
- * content itself. The elements dispatch `cito-feedback-show` here.
13
+ * content itself. The elements dispatch `qc-feedback-show` here.
14
14
  */
15
15
  protected feedbackShowing(): boolean;
16
16
  /** Called when this feedback is about to hide. Return `false` to keep it showing: the host hides it itself. */
@@ -99,7 +99,7 @@ export class QtiFeedback extends LitElement {
99
99
  }
100
100
  /**
101
101
  * Called when this feedback is about to show. Return `false` to keep it hidden: the host shows the
102
- * content itself. The elements dispatch `cito-feedback-show` here.
102
+ * content itself. The elements dispatch `qc-feedback-show` here.
103
103
  */
104
104
  feedbackShowing() {
105
105
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"qti-feedback.js","sourceRoot":"","sources":["../../src/abstract/qti-feedback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextConsumer, consume } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { computedContext } from '../context/computed.context';\nimport { itemContext } from '../context/item.context';\nimport { IsNullOrUndefined } from '../utils/utils';\n\nimport type { ComputedContext } from '../context/types/computed.types';\nimport type { ItemContext } from '../context/types/item.types';\n\nexport abstract class QtiFeedback extends LitElement {\n @property({ type: String, attribute: 'show-hide' })\n protected showHide: 'show' | 'hide' = 'show';\n\n @property({ type: String, attribute: 'outcome-identifier' })\n public outcomeIdentifier: string;\n\n @property({ type: String })\n protected identifier: string;\n\n @property({ type: String, attribute: false })\n public showStatus: string;\n\n /**\n * The item's variables. Feedback follows them itself: every time the item publishes a new context\n * this decides, synchronously, whether its own outcome now calls for showing it, so the item has\n * no list of feedback elements to push to. Synchronous matters. A host that sets an outcome\n * reads `showStatus` straight after, with no await in between.\n */\n #itemContext = new ContextConsumer(this, {\n context: itemContext,\n subscribe: true,\n callback: context => this.#followItem(context)\n });\n\n private get _context(): ItemContext | undefined {\n return this.#itemContext.value;\n }\n\n /** What the last context carried that decides this feedback, to tell a relevant update from noise. */\n #seen?: string;\n\n @consume({ context: computedContext, subscribe: true })\n @state()\n private _computedContext?: ComputedContext;\n\n /**\n * Re-decide when this feedback's outcome changed, or once an attempt has been made. Before the\n * first attempt an unchanged outcome says nothing yet: a `hide` feedback would otherwise switch\n * on for an item nobody has answered.\n */\n #followItem(context: ItemContext | undefined) {\n const variables = context?.variables;\n if (!variables || !this.outcomeIdentifier) return;\n const outcome = JSON.stringify(variables.find(v => v.identifier === this.outcomeIdentifier)?.value ?? null);\n const numAttempts = Number(variables.find(v => v.identifier === 'numAttempts')?.value) || 0;\n const relevant = this.#seen === undefined ? numAttempts > 0 : this.#seen !== outcome || numAttempts > 0;\n this.#seen = outcome;\n if (relevant) this.checkShowFeedback(this.outcomeIdentifier);\n }\n\n public checkShowFeedback(outcomeIdentifier: string) {\n const outcomeVariable = this._context.variables.find(v => v.identifier === outcomeIdentifier) || null;\n if (this.outcomeIdentifier !== outcomeIdentifier || !outcomeVariable) return;\n let isFound = false;\n if (Array.isArray(outcomeVariable.value)) {\n isFound = outcomeVariable.value.includes(this.identifier);\n } else {\n isFound =\n (!IsNullOrUndefined(this.identifier) &&\n !IsNullOrUndefined(outcomeVariable?.value) &&\n this.identifier === outcomeVariable.value) ||\n false;\n }\n\n this.#showFeedback(isFound);\n }\n\n /** The host cancelled the show: it has this feedback, and is not asked again until the condition has gone. */\n #hostShows = false;\n\n #showFeedback(value: boolean) {\n const next = this.#sessionControlAllowsFeedback() && this.#conditionHolds(value) ? 'on' : 'off';\n if (next === 'on') {\n if (this.showStatus === 'on' || this.#hostShows) return;\n if (!this.feedbackShowing()) {\n // The host takes over showing: this stays off and the host shows the content itself.\n this.#hostShows = true;\n this.showStatus = 'off';\n return;\n }\n } else {\n this.#hostShows = false;\n if (this.showStatus === 'on' && !this.feedbackHiding()) return;\n }\n this.showStatus = next;\n }\n\n #conditionHolds(value: boolean) {\n return (value && this.showHide === 'show') || (!value && this.showHide === 'hide');\n }\n\n /**\n * Called when this feedback is about to show. Return `false` to keep it hidden: the host shows the\n * content itself. The elements dispatch `cito-feedback-show` here.\n */\n protected feedbackShowing(): boolean {\n return true;\n }\n\n /** Called when this feedback is about to hide. Return `false` to keep it showing: the host hides it itself. */\n protected feedbackHiding(): boolean {\n return true;\n }\n\n /**\n * The QTI `show-feedback` constraint governs exactly one state: after the end of\n * the last attempt. Up to that point the spec requires any applicable feedback to\n * be shown — \"a value of max-attempts greater than 1, by definition, indicates\n * that any applicable feedback must be shown\" — and only \"once the maximum number\n * of allowed attempts have been used (or for adaptive items, completionStatus has\n * been set to completed)\" does `show-feedback` decide.\n *\n * This only ever forces feedback off, so `show-feedback` being true settles it:\n * there is no state in which the constraint hides feedback. It defaults to\n * false, and an absent value suppresses once the attempts are gone.\n *\n * That leaves one question — is the item out of attempts? — and the two item\n * kinds answer it from different variables, so each reads only its own.\n */\n #sessionControlAllowsFeedback(): boolean {\n if (!this._computedContext) return true;\n const activeItem = this._computedContext.testParts\n ?.find(tp => tp.active)\n ?.sections.flatMap(s => s.items)\n .find(i => i.active);\n if (!activeItem || activeItem.showFeedback) return true;\n\n // Adaptive items ignore max-attempts: \"the number of attempts is limited by\n // the value of the completionStatus built-in outcome variable\".\n if (activeItem.adaptive) return activeItem.completionStatus !== 'completed';\n\n // `max-attempts=\"0\"` is \"no limit\", so the last attempt never arrives.\n const maxAttempts = activeItem.maxAttempts ?? 1;\n if (maxAttempts <= 0) return true;\n\n const numAttempts = Number(this._context?.variables?.find(v => v.identifier === 'numAttempts')?.value) || 0;\n return numAttempts < maxAttempts;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-feedback.js","sourceRoot":"","sources":["../../src/abstract/qti-feedback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContextConsumer, consume } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { computedContext } from '../context/computed.context';\nimport { itemContext } from '../context/item.context';\nimport { IsNullOrUndefined } from '../utils/utils';\n\nimport type { ComputedContext } from '../context/types/computed.types';\nimport type { ItemContext } from '../context/types/item.types';\n\nexport abstract class QtiFeedback extends LitElement {\n @property({ type: String, attribute: 'show-hide' })\n protected showHide: 'show' | 'hide' = 'show';\n\n @property({ type: String, attribute: 'outcome-identifier' })\n public outcomeIdentifier: string;\n\n @property({ type: String })\n protected identifier: string;\n\n @property({ type: String, attribute: false })\n public showStatus: string;\n\n /**\n * The item's variables. Feedback follows them itself: every time the item publishes a new context\n * this decides, synchronously, whether its own outcome now calls for showing it, so the item has\n * no list of feedback elements to push to. Synchronous matters. A host that sets an outcome\n * reads `showStatus` straight after, with no await in between.\n */\n #itemContext = new ContextConsumer(this, {\n context: itemContext,\n subscribe: true,\n callback: context => this.#followItem(context)\n });\n\n private get _context(): ItemContext | undefined {\n return this.#itemContext.value;\n }\n\n /** What the last context carried that decides this feedback, to tell a relevant update from noise. */\n #seen?: string;\n\n @consume({ context: computedContext, subscribe: true })\n @state()\n private _computedContext?: ComputedContext;\n\n /**\n * Re-decide when this feedback's outcome changed, or once an attempt has been made. Before the\n * first attempt an unchanged outcome says nothing yet: a `hide` feedback would otherwise switch\n * on for an item nobody has answered.\n */\n #followItem(context: ItemContext | undefined) {\n const variables = context?.variables;\n if (!variables || !this.outcomeIdentifier) return;\n const outcome = JSON.stringify(variables.find(v => v.identifier === this.outcomeIdentifier)?.value ?? null);\n const numAttempts = Number(variables.find(v => v.identifier === 'numAttempts')?.value) || 0;\n const relevant = this.#seen === undefined ? numAttempts > 0 : this.#seen !== outcome || numAttempts > 0;\n this.#seen = outcome;\n if (relevant) this.checkShowFeedback(this.outcomeIdentifier);\n }\n\n public checkShowFeedback(outcomeIdentifier: string) {\n const outcomeVariable = this._context.variables.find(v => v.identifier === outcomeIdentifier) || null;\n if (this.outcomeIdentifier !== outcomeIdentifier || !outcomeVariable) return;\n let isFound = false;\n if (Array.isArray(outcomeVariable.value)) {\n isFound = outcomeVariable.value.includes(this.identifier);\n } else {\n isFound =\n (!IsNullOrUndefined(this.identifier) &&\n !IsNullOrUndefined(outcomeVariable?.value) &&\n this.identifier === outcomeVariable.value) ||\n false;\n }\n\n this.#showFeedback(isFound);\n }\n\n /** The host cancelled the show: it has this feedback, and is not asked again until the condition has gone. */\n #hostShows = false;\n\n #showFeedback(value: boolean) {\n const next = this.#sessionControlAllowsFeedback() && this.#conditionHolds(value) ? 'on' : 'off';\n if (next === 'on') {\n if (this.showStatus === 'on' || this.#hostShows) return;\n if (!this.feedbackShowing()) {\n // The host takes over showing: this stays off and the host shows the content itself.\n this.#hostShows = true;\n this.showStatus = 'off';\n return;\n }\n } else {\n this.#hostShows = false;\n if (this.showStatus === 'on' && !this.feedbackHiding()) return;\n }\n this.showStatus = next;\n }\n\n #conditionHolds(value: boolean) {\n return (value && this.showHide === 'show') || (!value && this.showHide === 'hide');\n }\n\n /**\n * Called when this feedback is about to show. Return `false` to keep it hidden: the host shows the\n * content itself. The elements dispatch `qc-feedback-show` here.\n */\n protected feedbackShowing(): boolean {\n return true;\n }\n\n /** Called when this feedback is about to hide. Return `false` to keep it showing: the host hides it itself. */\n protected feedbackHiding(): boolean {\n return true;\n }\n\n /**\n * The QTI `show-feedback` constraint governs exactly one state: after the end of\n * the last attempt. Up to that point the spec requires any applicable feedback to\n * be shown — \"a value of max-attempts greater than 1, by definition, indicates\n * that any applicable feedback must be shown\" — and only \"once the maximum number\n * of allowed attempts have been used (or for adaptive items, completionStatus has\n * been set to completed)\" does `show-feedback` decide.\n *\n * This only ever forces feedback off, so `show-feedback` being true settles it:\n * there is no state in which the constraint hides feedback. It defaults to\n * false, and an absent value suppresses once the attempts are gone.\n *\n * That leaves one question — is the item out of attempts? — and the two item\n * kinds answer it from different variables, so each reads only its own.\n */\n #sessionControlAllowsFeedback(): boolean {\n if (!this._computedContext) return true;\n const activeItem = this._computedContext.testParts\n ?.find(tp => tp.active)\n ?.sections.flatMap(s => s.items)\n .find(i => i.active);\n if (!activeItem || activeItem.showFeedback) return true;\n\n // Adaptive items ignore max-attempts: \"the number of attempts is limited by\n // the value of the completionStatus built-in outcome variable\".\n if (activeItem.adaptive) return activeItem.completionStatus !== 'completed';\n\n // `max-attempts=\"0\"` is \"no limit\", so the last attempt never arrives.\n const maxAttempts = activeItem.maxAttempts ?? 1;\n if (maxAttempts <= 0) return true;\n\n const numAttempts = Number(this._context?.variables?.find(v => v.identifier === 'numAttempts')?.value) || 0;\n return numAttempts < maxAttempts;\n }\n}\n"]}
@@ -11,7 +11,7 @@ export interface SessionContext {
11
11
  /**
12
12
  * What test plugins keep, by plugin name: JSON the test stores and restores without reading it.
13
13
  * Part of the persisted state (`QtiTestState.session.plugins`); written through the
14
- * `cito-test-update-plugin-state` request.
14
+ * `qc-test-update-plugin-state` request.
15
15
  */
16
16
  plugins?: Readonly<Record<string, unknown>>;
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session.context.js","sourceRoot":"","sources":["../../src/context/session.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAqB7C,MAAM,CAAC,MAAM,uBAAuB,GAA6B,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAEvF,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAA2B,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC","sourcesContent":["import { createContext } from '@lit/context';\n\nexport type View = 'author' | 'candidate' | 'proctor' | 'scorer' | 'testConstructor' | 'tutor' | '';\n\nexport interface SessionContext {\n navPartId?: string | null;\n navSectionId?: string | null;\n navItemRefId?: string | null;\n /** @deprecated Never set by the library. Removed in the next major. */\n navItemLoading?: boolean;\n /** @deprecated Never set by the library. Removed in the next major. */\n navTestLoading?: boolean;\n view?: View;\n /**\n * What test plugins keep, by plugin name: JSON the test stores and restores without reading it.\n * Part of the persisted state (`QtiTestState.session.plugins`); written through the\n * `cito-test-update-plugin-state` request.\n */\n plugins?: Readonly<Record<string, unknown>>;\n}\n\nexport const INITIAL_SESSION_CONTEXT: Readonly<SessionContext> = { view: 'candidate' };\n\nexport const sessionContext = createContext<Readonly<SessionContext>>(Symbol('sessionContext'));\n"]}
1
+ {"version":3,"file":"session.context.js","sourceRoot":"","sources":["../../src/context/session.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAqB7C,MAAM,CAAC,MAAM,uBAAuB,GAA6B,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAEvF,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAA2B,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC","sourcesContent":["import { createContext } from '@lit/context';\n\nexport type View = 'author' | 'candidate' | 'proctor' | 'scorer' | 'testConstructor' | 'tutor' | '';\n\nexport interface SessionContext {\n navPartId?: string | null;\n navSectionId?: string | null;\n navItemRefId?: string | null;\n /** @deprecated Never set by the library. Removed in the next major. */\n navItemLoading?: boolean;\n /** @deprecated Never set by the library. Removed in the next major. */\n navTestLoading?: boolean;\n view?: View;\n /**\n * What test plugins keep, by plugin name: JSON the test stores and restores without reading it.\n * Part of the persisted state (`QtiTestState.session.plugins`); written through the\n * `qc-test-update-plugin-state` request.\n */\n plugins?: Readonly<Record<string, unknown>>;\n}\n\nexport const INITIAL_SESSION_CONTEXT: Readonly<SessionContext> = { view: 'candidate' };\n\nexport const sessionContext = createContext<Readonly<SessionContext>>(Symbol('sessionContext'));\n"]}
@@ -18,7 +18,7 @@ export type ComputedItem = ComputedItemContext & {
18
18
  };
19
19
  export type ComputedContext = {
20
20
  view: View;
21
- /** What test plugins keep, by plugin name (`QtiTestState.session.plugins`). Read-only; write with `cito-test-update-plugin-state`. */
21
+ /** What test plugins keep, by plugin name (`QtiTestState.session.plugins`). Read-only; write with `qc-test-update-plugin-state`. */
22
22
  plugins?: Readonly<Record<string, unknown>>;
23
23
  identifier: string;
24
24
  title: string;
@@ -1 +1 @@
1
- {"version":3,"file":"computed.types.js","sourceRoot":"","sources":["../../../src/context/types/computed.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { View } from '../session.context';\nimport type { ComputedItemContext } from './computed-item.types';\n\nexport type ComputedItem = ComputedItemContext & {\n categories?: string[]; // not necessary for outside world\n type?: 'info' | 'regular';\n index?: number;\n active?: boolean;\n allowSkipping?: boolean;\n maxAttempts?: number;\n showFeedback?: boolean;\n showSolution?: boolean;\n numAttempts?: number;\n valid?: boolean;\n isDefaultResponse?: boolean;\n done?: boolean;\n /** Last *ended attempt* reached the optimal outcome (best achievable score / correct response). */\n optimal?: boolean;\n};\n\nexport type ComputedContext = {\n view: View;\n /** What test plugins keep, by plugin name (`QtiTestState.session.plugins`). Read-only; write with `cito-test-update-plugin-state`. */\n plugins?: Readonly<Record<string, unknown>>;\n identifier: string;\n title: string;\n testParts: {\n active?: boolean;\n identifier: string;\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n allowSkipping?: boolean;\n sections: {\n active?: boolean;\n identifier: string;\n title: string;\n completed?: boolean;\n items: ComputedItem[];\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n allowSkipping?: boolean;\n }[];\n }[];\n /**\n * The rendered `qti-assessment-item` for an item-ref identifier, or null when it is not on the\n * page. Looked up when called, so it is never a stale element; ask again after the context\n * updates (it does when an item is rendered). A function, so `JSON.stringify` leaves it out.\n * Set by `test-navigation`; for plugins that read an item's content (e.g. text-to-speech).\n */\n itemElement?: (identifier: string) => HTMLElement | null;\n};\n"]}
1
+ {"version":3,"file":"computed.types.js","sourceRoot":"","sources":["../../../src/context/types/computed.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { View } from '../session.context';\nimport type { ComputedItemContext } from './computed-item.types';\n\nexport type ComputedItem = ComputedItemContext & {\n categories?: string[]; // not necessary for outside world\n type?: 'info' | 'regular';\n index?: number;\n active?: boolean;\n allowSkipping?: boolean;\n maxAttempts?: number;\n showFeedback?: boolean;\n showSolution?: boolean;\n numAttempts?: number;\n valid?: boolean;\n isDefaultResponse?: boolean;\n done?: boolean;\n /** Last *ended attempt* reached the optimal outcome (best achievable score / correct response). */\n optimal?: boolean;\n};\n\nexport type ComputedContext = {\n view: View;\n /** What test plugins keep, by plugin name (`QtiTestState.session.plugins`). Read-only; write with `qc-test-update-plugin-state`. */\n plugins?: Readonly<Record<string, unknown>>;\n identifier: string;\n title: string;\n testParts: {\n active?: boolean;\n identifier: string;\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n allowSkipping?: boolean;\n sections: {\n active?: boolean;\n identifier: string;\n title: string;\n completed?: boolean;\n items: ComputedItem[];\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n allowSkipping?: boolean;\n }[];\n }[];\n /**\n * The rendered `qti-assessment-item` for an item-ref identifier, or null when it is not on the\n * page. Looked up when called, so it is never a stale element; ask again after the context\n * updates (it does when an item is rendered). A function, so `JSON.stringify` leaves it out.\n * Set by `test-navigation`; for plugins that read an item's content (e.g. text-to-speech).\n */\n itemElement?: (identifier: string) => HTMLElement | null;\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -27,5 +27,6 @@ export * from './lib/record';
27
27
  export * from './lib/qti-rule-base';
28
28
  export * from './lib/custom-operators';
29
29
  export * from './utils/custom-state-set';
30
+ export * from './utils/state-aliases';
30
31
  export * from './utils/scoring-helper';
31
32
  export * from './utils/utils';
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ export * from './lib/record';
27
27
  export * from './lib/qti-rule-base';
28
28
  export * from './lib/custom-operators';
29
29
  export * from './utils/custom-state-set';
30
+ export * from './utils/state-aliases';
30
31
  export * from './utils/scoring-helper';
31
32
  export * from './utils/utils';
32
33
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAChC,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './context/context-root';\nimport './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/record';\nexport * from './lib/qti-rule-base';\nexport * from './lib/custom-operators';\n\nexport * from './utils/custom-state-set';\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAChC,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './context/context-root';\nimport './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/record';\nexport * from './lib/qti-rule-base';\nexport * from './lib/custom-operators';\n\nexport * from './utils/custom-state-set';\nexport * from './utils/state-aliases';\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The vocabulary of custom states (CONTRACT.md §3 in qti-theme), with the names it replaced.
3
+ *
4
+ * A state is set under the name the contract uses. For one major the old name is set too, so a
5
+ * host that still reads `:state(drag)` keeps working while the theme reads `:state(chip)`. When the
6
+ * alias period ends, empty the table; every call site then sets only the new name.
7
+ *
8
+ * Not in the table, because the old name meant something else on another element:
9
+ * - a drop target's `drag-active` replaced `active` + `enabled`; the interaction host's `drag-active`
10
+ * replaced `dragzone-active` + `dragzone-enabled`. Each call site names its own old pair.
11
+ * - a drop target's `full` replaced the attribute `disabled`, which stays as an attribute.
12
+ * - `answer-key` replaced `show-correct-response` on an interaction host and `correct-response` on a
13
+ * choice; the corrections package passes the one that applies.
14
+ */
15
+ export declare const STATE_ALIASES: Readonly<Record<string, readonly string[]>>;
16
+ type StateSet = Pick<Set<string>, 'add' | 'delete' | 'has'>;
17
+ /**
18
+ * Set or clear a state and its aliases. `states` may be undefined on an element without internals;
19
+ * then nothing happens, as before.
20
+ */
21
+ export declare function setState(states: StateSet | undefined | null, name: string, on: boolean, oldNames?: readonly string[]): void;
22
+ /** Whether a state is set, under its contract name or an alias. */
23
+ export declare function hasState(states: StateSet | undefined | null, name: string): boolean;
24
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The vocabulary of custom states (CONTRACT.md §3 in qti-theme), with the names it replaced.
3
+ *
4
+ * A state is set under the name the contract uses. For one major the old name is set too, so a
5
+ * host that still reads `:state(drag)` keeps working while the theme reads `:state(chip)`. When the
6
+ * alias period ends, empty the table; every call site then sets only the new name.
7
+ *
8
+ * Not in the table, because the old name meant something else on another element:
9
+ * - a drop target's `drag-active` replaced `active` + `enabled`; the interaction host's `drag-active`
10
+ * replaced `dragzone-active` + `dragzone-enabled`. Each call site names its own old pair.
11
+ * - a drop target's `full` replaced the attribute `disabled`, which stays as an attribute.
12
+ * - `answer-key` replaced `show-correct-response` on an interaction host and `correct-response` on a
13
+ * choice; the corrections package passes the one that applies.
14
+ */
15
+ export const STATE_ALIASES = {
16
+ chip: ['drag'],
17
+ drop: ['droppable'],
18
+ lifted: ['dragging'],
19
+ used: ['placeholder'],
20
+ 'return-target': ['return-anchor'],
21
+ 'drag-over': ['hover'],
22
+ 'candidate-partial': ['candidate-partially-correct']
23
+ };
24
+ /**
25
+ * Set or clear a state and its aliases. `states` may be undefined on an element without internals;
26
+ * then nothing happens, as before.
27
+ */
28
+ export function setState(states, name, on, oldNames) {
29
+ if (!states)
30
+ return;
31
+ const names = [name, ...(oldNames ?? STATE_ALIASES[name] ?? [])];
32
+ for (const n of names) {
33
+ if (on)
34
+ states.add(n);
35
+ else
36
+ states.delete(n);
37
+ }
38
+ }
39
+ /** Whether a state is set, under its contract name or an alias. */
40
+ export function hasState(states, name) {
41
+ if (!states)
42
+ return false;
43
+ if (states.has(name))
44
+ return true;
45
+ return (STATE_ALIASES[name] ?? []).some(n => states.has(n));
46
+ }
47
+ //# sourceMappingURL=state-aliases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-aliases.js","sourceRoot":"","sources":["../../src/utils/state-aliases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,aAAa,GAAgD;IACxE,IAAI,EAAE,CAAC,MAAM,CAAC;IACd,IAAI,EAAE,CAAC,WAAW,CAAC;IACnB,MAAM,EAAE,CAAC,UAAU,CAAC;IACpB,IAAI,EAAE,CAAC,aAAa,CAAC;IACrB,eAAe,EAAE,CAAC,eAAe,CAAC;IAClC,WAAW,EAAE,CAAC,OAAO,CAAC;IACtB,mBAAmB,EAAE,CAAC,6BAA6B,CAAC;CACrD,CAAC;AAIF;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,MAAmC,EACnC,IAAY,EACZ,EAAW,EACX,QAA4B;IAE5B,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACjE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,EAAE;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;;YACjB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,QAAQ,CAAC,MAAmC,EAAE,IAAY;IACxE,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * The vocabulary of custom states (CONTRACT.md §3 in qti-theme), with the names it replaced.\n *\n * A state is set under the name the contract uses. For one major the old name is set too, so a\n * host that still reads `:state(drag)` keeps working while the theme reads `:state(chip)`. When the\n * alias period ends, empty the table; every call site then sets only the new name.\n *\n * Not in the table, because the old name meant something else on another element:\n * - a drop target's `drag-active` replaced `active` + `enabled`; the interaction host's `drag-active`\n * replaced `dragzone-active` + `dragzone-enabled`. Each call site names its own old pair.\n * - a drop target's `full` replaced the attribute `disabled`, which stays as an attribute.\n * - `answer-key` replaced `show-correct-response` on an interaction host and `correct-response` on a\n * choice; the corrections package passes the one that applies.\n */\nexport const STATE_ALIASES: Readonly<Record<string, readonly string[]>> = {\n chip: ['drag'],\n drop: ['droppable'],\n lifted: ['dragging'],\n used: ['placeholder'],\n 'return-target': ['return-anchor'],\n 'drag-over': ['hover'],\n 'candidate-partial': ['candidate-partially-correct']\n};\n\ntype StateSet = Pick<Set<string>, 'add' | 'delete' | 'has'>;\n\n/**\n * Set or clear a state and its aliases. `states` may be undefined on an element without internals;\n * then nothing happens, as before.\n */\nexport function setState(\n states: StateSet | undefined | null,\n name: string,\n on: boolean,\n oldNames?: readonly string[]\n): void {\n if (!states) return;\n const names = [name, ...(oldNames ?? STATE_ALIASES[name] ?? [])];\n for (const n of names) {\n if (on) states.add(n);\n else states.delete(n);\n }\n}\n\n/** Whether a state is set, under its contract name or an alias. */\nexport function hasState(states: StateSet | undefined | null, name: string): boolean {\n if (!states) return false;\n if (states.has(name)) return true;\n return (STATE_ALIASES[name] ?? []).some(n => states.has(n));\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qti-components/base",
3
3
  "description": "QTI shared utilities",
4
- "version": "3.0.0-next.5",
4
+ "version": "3.0.0-next.6",
5
5
  "author": "",
6
6
  "dependencies": {
7
7
  "@qti-components/utilities": "^1.3.3"