@qti-components/base 3.0.0-next.4 → 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.
- package/dist/abstract/interaction.d.ts +32 -1
- package/dist/abstract/interaction.js +79 -2
- package/dist/abstract/interaction.js.map +1 -1
- package/dist/abstract/qti-feedback.d.ts +7 -0
- package/dist/abstract/qti-feedback.js +34 -4
- package/dist/abstract/qti-feedback.js.map +1 -1
- package/dist/context/config.context.d.ts +2 -1
- package/dist/context/config.context.js.map +1 -1
- package/dist/context/session.context.d.ts +1 -1
- package/dist/context/session.context.js.map +1 -1
- package/dist/context/types/computed.types.d.ts +1 -1
- package/dist/context/types/computed.types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/state-aliases.d.ts +24 -0
- package/dist/utils/state-aliases.js +47 -0
- package/dist/utils/state-aliases.js.map +1 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"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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';\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"]}
|
|
@@ -8,4 +8,11 @@ export declare abstract class QtiFeedback extends LitElement {
|
|
|
8
8
|
private get _context();
|
|
9
9
|
private _computedContext?;
|
|
10
10
|
checkShowFeedback(outcomeIdentifier: string): void;
|
|
11
|
+
/**
|
|
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 `qc-feedback-show` here.
|
|
14
|
+
*/
|
|
15
|
+
protected feedbackShowing(): boolean;
|
|
16
|
+
/** Called when this feedback is about to hide. Return `false` to keep it showing: the host hides it itself. */
|
|
17
|
+
protected feedbackHiding(): boolean;
|
|
11
18
|
}
|
|
@@ -25,6 +25,8 @@ export class QtiFeedback extends LitElement {
|
|
|
25
25
|
subscribe: true,
|
|
26
26
|
callback: context => this.#followItem(context)
|
|
27
27
|
});
|
|
28
|
+
/** The host cancelled the show: it has this feedback, and is not asked again until the condition has gone. */
|
|
29
|
+
this.#hostShows = false;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* The item's variables. Feedback follows them itself: every time the item publishes a new context
|
|
@@ -71,12 +73,40 @@ export class QtiFeedback extends LitElement {
|
|
|
71
73
|
}
|
|
72
74
|
this.#showFeedback(isFound);
|
|
73
75
|
}
|
|
76
|
+
/** The host cancelled the show: it has this feedback, and is not asked again until the condition has gone. */
|
|
77
|
+
#hostShows;
|
|
74
78
|
#showFeedback(value) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
const next = this.#sessionControlAllowsFeedback() && this.#conditionHolds(value) ? 'on' : 'off';
|
|
80
|
+
if (next === 'on') {
|
|
81
|
+
if (this.showStatus === 'on' || this.#hostShows)
|
|
82
|
+
return;
|
|
83
|
+
if (!this.feedbackShowing()) {
|
|
84
|
+
// The host takes over showing: this stays off and the host shows the content itself.
|
|
85
|
+
this.#hostShows = true;
|
|
86
|
+
this.showStatus = 'off';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.#hostShows = false;
|
|
92
|
+
if (this.showStatus === 'on' && !this.feedbackHiding())
|
|
93
|
+
return;
|
|
78
94
|
}
|
|
79
|
-
this.showStatus =
|
|
95
|
+
this.showStatus = next;
|
|
96
|
+
}
|
|
97
|
+
#conditionHolds(value) {
|
|
98
|
+
return (value && this.showHide === 'show') || (!value && this.showHide === 'hide');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
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 `qc-feedback-show` here.
|
|
103
|
+
*/
|
|
104
|
+
feedbackShowing() {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/** Called when this feedback is about to hide. Return `false` to keep it showing: the host hides it itself. */
|
|
108
|
+
feedbackHiding() {
|
|
109
|
+
return true;
|
|
80
110
|
}
|
|
81
111
|
/**
|
|
82
112
|
* The QTI `show-feedback` constraint governs exactly one state: after the end of
|
|
@@ -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 #showFeedback(value: boolean) {\n if (!this.#sessionControlAllowsFeedback()) {\n this.showStatus = 'off';\n return;\n }\n this.showStatus = (value && this.showHide === 'show') || (!value && this.showHide === 'hide') ? 'on' : 'off';\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"]}
|
|
@@ -57,7 +57,8 @@ export interface ConfigContext {
|
|
|
57
57
|
validationDisplayMode?: ValidationDisplayMode;
|
|
58
58
|
/**
|
|
59
59
|
* If true, disables further user actions when an interaction reaches its configured maximum.
|
|
60
|
-
* Applies to both choice-based and drag/drop interactions.
|
|
60
|
+
* Applies to both choice-based and drag/drop interactions. Defaults to false: QTI lets the candidate
|
|
61
|
+
* go over the maximum and reports it with the max-selections message.
|
|
61
62
|
*/
|
|
62
63
|
disableAfterMaxReached?: boolean;
|
|
63
64
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.context.js","sourceRoot":"","sources":["../../src/context/config.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"config.context.js","sourceRoot":"","sources":["../../src/context/config.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAmG7C,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAA0B,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC","sourcesContent":["import { createContext } from '@lit/context';\n\nexport type ValidationDisplayMode = 'inline' | 'native' | 'both' | 'none';\n\n/**\n * Configuration context for QTI components. Provides runtime options for assessment item and\n * interaction behavior.\n *\n * ── Three ways to set it, in the order you should reach for them ─────────────────────────────\n *\n * 1. **On a provider**, for real delivery. `qti-test` and `qti-item` both provide it, so setting\n * `item.configContext = {…}` configures every interaction inside.\n *\n * 2. **On one interaction, directly** — the development-time route, and the reason\n * `Interaction.configContext` is public:\n *\n * orderInteraction.configContext = { allowReorder: false };\n *\n * or, in a lit-html template, as a property binding — which lands before `connectedCallback`:\n *\n * html`<qti-order-interaction .configContext=${{ allowReorder: false }}>…`\n *\n * An interaction has to work standalone, so in a story or a spec there is usually no provider\n * above it, and then nothing ever overwrites what you set. Where a provider DOES exist it wins on\n * its next emit, because `@consume` writes that same field.\n *\n * A `qti-config-test-provider` element used to exist for scoping config to a subtree. It is gone:\n * it was test-only yet registered itself into every consumer's element registry via an unguarded\n * `@customElement`, which threw whenever two copies of this package ended up in one module graph.\n * Bind the property on each interaction instead, or provide from `qti-item`.\n *\n * There is no merging at any level: @lit/context resolves to the nearest provider and that object\n * replaces the ancestor's wholesale. A deeper provider therefore has to restate anything from above\n * that it still wants.\n *\n * Note what is NOT here. Options an item author chooses per question are attributes on the\n * interaction (`auto-size-dropzones`, `max-associations`, `disable-animations`); options only a\n * developer chooses are mixin factory arguments or method overrides. This context is for what a\n * delivery environment decides across items. `interactionContext` and `dragDropContext` are neither\n * — they publish derived state (what a choice IS, what a drop HOLDS), never configuration.\n */\nexport interface ConfigContext {\n /**\n * Optional category label for info items, used for reporting or display purposes.\n * Example: \"General Information\", \"Instructions\".\n */\n infoItemCategory?: string;\n\n /**\n * If true, calls reportValidity() on interactions after scoring to show validation feedback.\n * Used to trigger UI validation after a scoring event.\n */\n reportValidityAfterScoring?: boolean;\n\n /**\n * Controls how validation feedback is displayed by interactions:\n * - 'inline': render message in #validation-message (default behavior)\n * - 'native': use browser-native reportValidity popup behavior\n * - 'both': show both inline and native feedback\n * - 'none': keep validity state only, without visual feedback\n */\n validationDisplayMode?: ValidationDisplayMode;\n\n /**\n * If true, disables further user actions when an interaction reaches its configured maximum.\n * Applies to both choice-based and drag/drop interactions. Defaults to false: QTI lets the candidate\n * go over the maximum and reports it with the max-selections message.\n */\n disableAfterMaxReached?: boolean;\n\n /**\n * If false, chips already placed in a drop target cannot be reordered or moved between targets in\n * drag/drop sortable interactions (order, match, gap-match). Defaults to true. A per-interaction\n * `allowReorder` property overrides this.\n */\n allowReorder?: boolean;\n\n /**\n * If true, disables further selection in choice interactions after the maximum number of choices is reached.\n * @deprecated Legacy alias for choice interactions. Prefer disableAfterMaxReached.\n */\n disableAfterIfMaxChoicesReached?: boolean;\n\n /**\n * Optional prompt text to display in inline choice interactions when no option is selected.\n * Example: \"Select an answer...\"\n */\n inlineChoicePrompt?: string;\n\n /**\n * The inline choice interaction measures the widest option and sizes the trigger to match, so\n * choosing an option never changes the field's width. Defaults to true; set false to size the\n * field to the option shown.\n * When a `qti-input-width-*` class is present on the interaction, that class width always takes\n * precedence and autosizing is skipped regardless of this setting.\n */\n inlineChoiceAutosize?: boolean;\n}\n\nexport const configContext = createContext<Readonly<ConfigContext>>(Symbol('configContext'));\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
|
-
* `
|
|
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 * `
|
|
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 `
|
|
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 `
|
|
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"]}
|