// xapi-json-viewer.js — open one xAPI statement, pretty-printed and explained, in a new tab.
//
// Why a new tab and not an inline panel: MicroSims are embedded in fixed-height iframes, so
// an inline panel that grows to fit a ~60-line statement is either clipped or forces the
// iframe taller for everyone; a modal inside the iframe centres on the iframe's viewport,
// which can be off-screen when the reader has scrolled the parent page. A tab has the whole
// screen, can sit beside the sim, and does not disturb the lesson.
//
// Usage (from a click handler, so pop-up blockers allow it):
// XapiJsonViewer.open(statement, { source: 'Sine Wave MicroSim' });
//
// Nothing here posts anything. The tab is a Blob URL built from the statement in memory.
(function (global) {
'use strict';
var EXT = 'https://w3id.org/lrs/ext/';
// Keys that exist only because of compact mode — highlighted so a student sees at once
// what a summary adds to an ordinary statement.
var COMPACT_KEYS = ['statements_represented', 'controls', 'end_reason', 'xapi_mode',
'active_ms', 'session_ms', 'interaction_count', 'runs'];
function esc(s) {
return String(s).replace(/&/g, '&').replace(//g, '>');
}
// Colour one line of JSON.stringify(..., null, 2) output. Strings are matched first, so
// digits inside a UUID or IRI are never mistaken for numbers.
function colour(line) {
return esc(line).replace(
/("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,
function (m, str, colon, lit) {
if (str) return colon ? '' + str + '' + colon
: '' + str + '';
if (lit) return '' + lit + '';
return '' + m + '';
});
}
function isCompactLine(line) {
for (var i = 0; i < COMPACT_KEYS.length; i++) {
if (line.indexOf('"' + EXT + COMPACT_KEYS[i] + '"') !== -1) return true;
}
return false;
}
function get(obj, path) {
return path.split('.').reduce(function (o, k) { return o == null ? undefined : o[k]; }, obj);
}
// What each part of the statement means — only rows for fields actually present.
function legend(st) {
var rx = (st.result && st.result.extensions) || {};
var cx = (st.context && st.context.extensions) || {};
var rows = [
['actor', 'Who did it. The real account name enters here and is pseudonymized by the LRS, never by the sim.', st.actor],
['verb', 'What kind of act: interacted (a control), experienced (engagement over time), or answered (a question).', st.verb],
['object.id', 'Which activity. A page IRI, or page IRI + #fragment for one control on it.', get(st, 'object.id')],
['result.duration', 'Engaged time, as an ISO-8601 duration (PT12.5S = 12.5 seconds).', get(st, 'result.duration')],
['…/ext/value', 'The control\'s new value.', rx[EXT + 'value']],
['…/ext/previous-value', 'The value before this change, so one statement shows the direction of a move.', rx[EXT + 'previous-value']],
['…/ext/action', 'Which press of a Start/Pause control this was: start or pause. Carries no duration.', rx[EXT + 'action']],
['…/ext/run-ended-by', 'What closed this run interval: paused (the student), or a flush such as tab-hidden or simulated-done.', rx[EXT + 'run-ended-by']],
['…/ext/runs', 'Start/Pause run intervals folded into this summary: how many, and their total milliseconds.', rx[EXT + 'runs']],
['…/ext/controls', 'Per control, what compact mode folded: n movements, min/max/last value, the concept it is evidence for, and reversals (direction changes — the one thing a summary cannot recover later, because it has no order).', rx[EXT + 'controls']],
['…/ext/end_reason', 'Why the session ended: simulated-done, tab-hidden, scrolled-away, idle, mode-switch, …', rx[EXT + 'end_reason']],
['…/ext/active_ms', 'Milliseconds the student was plausibly engaged during the session.', rx[EXT + 'active_ms']],
['…/ext/concept_id', 'The concept this statement is evidence for. Mastery is rolled up per concept.', cx[EXT + 'concept_id']],
['…/ext/statements_represented', 'How many full-mode statements this one summary stands for.', cx[EXT + 'statements_represented']],
['context.contextActivities.grouping', 'The textbook and version the activity was used in.', get(st, 'context.contextActivities.grouping')]
];
return rows.filter(function (r) { return r[2] !== undefined; }).map(function (r) {
return '
' + r[0] + '
var verb = get(st, 'verb.display.en-US') || get(st, 'verb.id') || 'statement';
var name = get(st, 'object.definition.name.en-US') || get(st, 'object.id') || '';
var represented = get(st, 'context.extensions')
? st.context.extensions[EXT + 'statements_represented'] : undefined;
var compact = represented !== undefined;
var banner = compact
? ''
: '';
// Embed the JSON for the Copy/Download buttons; escape '<' so no string can close