@longsightgroup/qti3-player
v0.7.2
Published
Style-neutral web component player for rendering and scoring QTI 3 assessment items.
Maintainers
Readme
@longsightgroup/qti3-player
Style-neutral web component player for QTI 3 assessment items.
This package renders one QTI item at a time, captures responses, validates responses,
scores attempts through @longsightgroup/qti3-core, and emits host-readable state events.
Install
npm install @longsightgroup/qti3-playerUse
import { defineQtiAssessmentItemPlayer } from "@longsightgroup/qti3-player";
defineQtiAssessmentItemPlayer();<qti-assessment-item-player id="player"></qti-assessment-item-player>const player = document.querySelector("qti-assessment-item-player");
await player?.loadXml(xml, {
status: "interacting",
sessionControl: {
validateResponses: true,
showFeedback: true,
},
});
player?.addEventListener("qti-statechange", (event) => {
console.log(event.detail.state);
});Scoring Trust Boundary
The player can score attempts locally through @longsightgroup/qti3-core, but browser
scoring is a convenience for validation, feedback, previews, and response snapshots.
High-stakes assessment systems must treat browser outcomes as untrusted and recompute
scores server-side from authoritative QTI XML and trusted response variables.
Player Chrome Messages (host-owned i18n)
The player keeps authored QTI content (prompts, choice labels, title on
end-attempt) separate from player chrome (remove buttons, aria labels, live-region
status, gap labels).
Only English ships in the package. language-of-interface="sv-SE" does not
change chrome by itself; the host loads a locale file.
Locale files (recommended)
Harbor or an LMS maintains JSON (or ICU/Fluent exported to JSON) with the shape
PlayerMessageCatalog: a flat strings map, optional interactionTypes, and
directions. Copy defaultPlayerMessageCatalog from the package as the template; omit
keys you do not need to translate (missing keys fall back to English).
import type { PlayerMessageCatalog } from "@longsightgroup/qti3-player";
const sv = (await fetch("/locales/player/sv-SE.json")).json() as PlayerMessageCatalog;
player.languageOfInterface = "sv-SE";
player.messageCatalog = sv;
await player.loadXml(xml);Example entries:
{
"locale": "sv-SE",
"strings": {
"remove": "Ta bort",
"removePair": "Ta bort {label}",
"associationPairLabel": "{source} med {target}",
"extendedTextCounter": "{count} av {expectedLength}",
"associationsMade.one": "{count} koppling skapad.",
"associationsMade.other": "{count} kopplingar skapade."
},
"interactionTypes": {
"graphicOrder": "Grafisk ordning"
},
"directions": { "up": "upp", "down": "ner", "left": "vänster", "right": "höger" }
}Templates use {placeholder} names. For English-style singular/plural, use
messageKey.one and messageKey.other (for example associationsMade.one). Languages
that do not inflect by count can use a single key for plural message ids.
Validate locale files in CI with validatePlayerMessageCatalog() — pass JSON.parse output
directly (unknown). Shape errors (non-object root, missing strings, numeric templates, bad
directions / interactionTypes) return diagnostics instead of throwing:
import { validatePlayerMessageCatalog } from "@longsightgroup/qti3-player";
const raw = JSON.parse(await readFile("locales/player/sv-SE.json", "utf8"));
const result = validatePlayerMessageCatalog(raw);
if (!result.valid) {
for (const issue of result.diagnostics) {
console.error(`${issue.code} ${issue.key}: ${issue.message}`);
}
process.exit(1);
}Use requireAllKeys: true only for complete locale files forked from defaultPlayerMessageCatalog.
Partial delivery catalogs should omit that flag.
Reference exports:
PLAYER_MESSAGE_KEYS— message ids fromPLAYER_MESSAGE_MANIFESTPLAYER_MESSAGE_STRING_KEYS— all keys indefaultPlayerMessageCatalog.stringsdefaultPlayerMessageCatalog— English template to forkallowedCatalogPlaceholders(entry)— placeholders a template may userequiredCatalogPlaceholders(catalogKey)— placeholders required for that key per English defaultcreatePlayerMessageResolver(catalog)— canonical key-driven runtime APIPlayerMessageParams<K>— typed params formessage(key, params)
Resolver kinds in the manifest (hosts only edit strings; behavior is fixed):
| Resolver | Meaning |
| ------------------- | ---------------------------------------------------- |
| plain | Static string |
| template | {placeholder} interpolation from params |
| plural | Uses key.one / key.other when count is present |
| typeLabel | Interaction type short name from interactionTypes |
| typeTemplate | Template with {typeName} derived from type |
| directionTemplate | Template with localized {direction} |
Runtime API
Chrome resolves to a key-driven PlayerMessageResolver:
const messages = resolvePlayerMessages(locale, {}, catalog);
messages.message("remove");
messages.message("removePair", { label: pairLabel });
messages.message("associationsMade", { count: 2 });createPlayerMessageResolver(catalog) builds the same resolver directly from a locale file.
Per-message overrides
player.messages accepts QtiPlayerMessageOverrides — each key uses the same param types as
message(key, params) (for example removePair: ({ label }) => ...). Use only when a catalog
entry is not enough; composed strings (for example removePair using associationPairLabel
text) are easy to break.
Item language vs interface language
Do not copy item xml:lang onto <qti-assessment-item-player lang="..."> unless you
intentionally want the player element's lang attribute to influence
defaultPlayerLocale(). Prefer player.messageCatalog for UI chrome.
Portable Custom Interactions
For qti-portable-custom-interaction, the player renders a
qti3-portable-custom-host element, passes small module/configuration metadata through
dataset attributes, and emits qti-portable-custom-mount with the full parsed
definition. Host code can attach a PCI runtime and send response/state updates back with
qti3-portable-custom-response.
Production sandboxing, CSP, origin policy, and audit logging belong to the host delivery
system.
Framework adapters
Optional React and Preact TSX wrappers ship as separate packages. They keep the web component as the rendering primitive and only handle framework lifecycle wiring:
@longsightgroup/qti3-player-react@longsightgroup/qti3-player-preact
Use the native element directly when you do not need React or Preact integration.
Local manual proof for the React adapter: from the repo root run
pnpm dev:adapter-react and open /adapter-react.html (linked from the main manual harness).
Clearing a loaded item
clearItem() (or omitting declarative xml on the adapters) removes rendered content and in-memory
session state. It does not emit qti-statechange or other player events because no item is loaded.
Hosts should treat the prop transition or imperative call as the source of truth.
Framework adapters treat xml={undefined} as a clear and xml="" as a load attempt. An empty
string shows the parse error view when the XML is invalid.
Restored loadOptions.state reload keys use JSON serialization: equivalent content with different
object references does not reload, but key order follows construction order and in-place mutation
without a reload key change is not detected.
Styling
The player uses light DOM and is style-neutral by design. Host applications can style
the rendered qti3-* classes directly while preserving the item author's QTI shared
vocabulary classes.
Keyword emphasis
qti-keyword-emphasis is candidate-conditional. The player preserves the authored class
without applying special visual styling by default. After the host delivery system resolves
the candidate's AfA/PNP and finds keyword-emphasis, opt in before or after loading the
item:
player.keywordEmphasisEnabled = true;
// Equivalent DOM API:
player.setAttribute("data-keyword-emphasis", "true");When enabled, the rendered .qti3-player root receives data-keyword-emphasis="true" and
the bundled stylesheet visibly emphasizes .qti-keyword-emphasis. Set
player.keywordEmphasisEnabled = false or remove data-keyword-emphasis to return to the
default inert presentation.
Screen-reader status lines (.qti3-selection-summary, aria-live="polite") are
visually hidden by default so LMS shells do not show reorder or selection announcements
to sighted users. They remain available to assistive technology. Set
data-show-live-regions on qti-assessment-item-player only in local debug or harness
pages when you want those messages visible on screen.
See the main repository README for the support matrix and release notes: https://github.com/LongsightGroup/qti3
