// 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] + '' + r[1] + ''; }).join(''); } function page(st, opts) { var json = JSON.stringify(st, null, 2); var lines = json.split('\n'); var body = lines.map(function (l, i) { return '' + (i + 1) + '' + colour(l) + ''; }).join(''); // each .ln is a block already; a '\n' here would double-space the

    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 ';
  }

  function open(statement, opts) {
    var url = URL.createObjectURL(new Blob([page(statement, opts || {})], { type: 'text/html' }));
    // Not revoked: a few KB, and revoking would break reloading the tab.
    return global.open(url, '_blank');
  }

  global.XapiJsonViewer = { open: open, page: page };
})(typeof window !== 'undefined' ? window : this);