@patch-adams/core 1.5.10 → 1.5.13

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/index.js CHANGED
@@ -3394,6 +3394,95 @@ function generateLrsBridgeCode(options) {
3394
3394
  sendStatement(statement);
3395
3395
  };
3396
3396
 
3397
+ // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3398
+ LRS.sendCompletionStatement = function(data) {
3399
+ data = data || {};
3400
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3401
+ if (scormCompletionDebounce) {
3402
+ clearTimeout(scormCompletionDebounce);
3403
+ scormCompletionDebounce = null;
3404
+ }
3405
+
3406
+ var status = data.status || 'completed';
3407
+ var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
3408
+
3409
+ var verbKey = 'completed';
3410
+ if (status === 'passed') verbKey = 'passed';
3411
+ if (status === 'failed') verbKey = 'failed';
3412
+
3413
+ var result = { completion: true, success: (status === 'passed') };
3414
+
3415
+ if (typeof data.score === 'number') {
3416
+ var max = data.scoreMax || 100;
3417
+ var min = data.scoreMin || 0;
3418
+ result.score = {
3419
+ raw: data.score,
3420
+ max: max,
3421
+ min: min,
3422
+ scaled: max > min ? (data.score - min) / (max - min) : 0
3423
+ };
3424
+ }
3425
+
3426
+ var activityDetails = {
3427
+ courseTitle: courseTitle,
3428
+ completionStatus: status,
3429
+ employeeEmail: data.email || (LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined),
3430
+ employeeId: data.employeeId || LRS.employeeId || undefined
3431
+ };
3432
+ if (data.employeeName) activityDetails.employeeName = data.employeeName;
3433
+
3434
+ // 1. Always send "completed" statement
3435
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3436
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3437
+ sendStatement(completedStatement);
3438
+
3439
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3440
+ if (status === 'passed' || status === 'failed') {
3441
+ log('Sending', status, 'statement');
3442
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3443
+ sendStatement(outcomeStatement);
3444
+ }
3445
+ };
3446
+
3447
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3448
+ var exitStatementSent = false;
3449
+ LRS.sendExitStatement = function(data) {
3450
+ if (exitStatementSent) return;
3451
+ exitStatementSent = true;
3452
+ data = data || {};
3453
+
3454
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3455
+ var duration = null;
3456
+ if (LRS.launchTime) {
3457
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3458
+ var secs = Math.floor(ms / 1000);
3459
+ var mins = Math.floor(secs / 60);
3460
+ var hrs = Math.floor(mins / 60);
3461
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3462
+ }
3463
+
3464
+ var activityDetails = {
3465
+ courseTitle: courseTitle,
3466
+ exitSource: data.source || 'page-unload',
3467
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3468
+ employeeId: LRS.employeeId || undefined,
3469
+ sessionDuration: duration,
3470
+ statementsSent: LRS.stats.statementsSent
3471
+ };
3472
+
3473
+ var result = duration ? { duration: duration } : null;
3474
+ log('Sending exit statement, duration:', duration);
3475
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3476
+ sendStatement(statement);
3477
+ };
3478
+
3479
+ // Auto-send exit statement on page unload
3480
+ window.addEventListener('beforeunload', function() {
3481
+ if (!exitStatementSent && LRS.actor) {
3482
+ LRS.sendExitStatement({ source: 'page-unload' });
3483
+ }
3484
+ });
3485
+
3397
3486
  /**
3398
3487
  * Re-extract actor from SCORM after LMSInitialize has been called.
3399
3488
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3500,6 +3589,23 @@ function generateLrsBridgeCode(options) {
3500
3589
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3501
3590
  };
3502
3591
 
3592
+ // ========================================================================
3593
+ // PUBLIC API \u2014 expose core internals for skin scripts
3594
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3595
+ // ========================================================================
3596
+ LRS.api = {
3597
+ sendStatement: sendStatement,
3598
+ buildStatement: buildStatement,
3599
+ buildCourseActivityObject: buildCourseActivityObject,
3600
+ buildXylemeContext: buildXylemeContext,
3601
+ generateUUID: generateUUID,
3602
+ decodeEntities: decodeEntities,
3603
+ extractActor: extractActor,
3604
+ getCachedLessonInfo: getCachedLessonInfo,
3605
+ VERBS: VERBS,
3606
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3607
+ };
3608
+
3503
3609
  // ========================================================================
3504
3610
  // 8. RISE EVENT INTERCEPTORS
3505
3611
  // ========================================================================
@@ -4064,9 +4170,40 @@ function generateLrsBridgeCode(options) {
4064
4170
  var scormInteractions = {}; // Pending interactions keyed by index N
4065
4171
  var scormInteractionsSent = {}; // Track which interactions were already sent
4066
4172
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4173
+ var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4174
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4067
4175
 
4068
4176
  function interceptScormSetValue(key, value) {
4069
4177
  if (typeof key !== 'string') return;
4178
+
4179
+ // ---- Course-level SCORM data (score, status, completion) ----
4180
+ // SCORM 1.2
4181
+ if (key === 'cmi.core.score.raw') { scormCourseData.scoreRaw = parseFloat(value); }
4182
+ if (key === 'cmi.core.score.max') { scormCourseData.scoreMax = parseFloat(value); }
4183
+ if (key === 'cmi.core.score.min') { scormCourseData.scoreMin = parseFloat(value); }
4184
+ if (key === 'cmi.core.lesson_status') { scormCourseData.status = String(value).toLowerCase(); }
4185
+ // SCORM 2004
4186
+ if (key === 'cmi.score.raw') { scormCourseData.scoreRaw = parseFloat(value); }
4187
+ if (key === 'cmi.score.max') { scormCourseData.scoreMax = parseFloat(value); }
4188
+ if (key === 'cmi.score.min') { scormCourseData.scoreMin = parseFloat(value); }
4189
+ if (key === 'cmi.score.scaled') { scormCourseData.scoreScaled = parseFloat(value); }
4190
+ if (key === 'cmi.completion_status') { scormCourseData.completionStatus = String(value).toLowerCase(); }
4191
+ if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4192
+
4193
+ // Fire course completion statement when status indicates pass/fail/complete
4194
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4195
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4196
+ var status = String(value).toLowerCase();
4197
+ if (status === 'passed' || status === 'failed' || status === 'completed') {
4198
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4199
+ scormCompletionDebounce = setTimeout(function() {
4200
+ scormCompletionDebounce = null;
4201
+ sendCourseCompletionStatement(status);
4202
+ }, 500);
4203
+ }
4204
+ }
4205
+
4206
+ // ---- Interaction-level tracking (existing) ----
4070
4207
  var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4071
4208
  var match = key.match(interactionPattern);
4072
4209
  if (!match) return;
@@ -4166,6 +4303,53 @@ function generateLrsBridgeCode(options) {
4166
4303
  }
4167
4304
  }
4168
4305
 
4306
+ function sendCourseCompletionStatement(status) {
4307
+ logGroup('Course Completion');
4308
+ log('Status:', status);
4309
+ log('SCORM course data:', scormCourseData);
4310
+
4311
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4312
+
4313
+ // Build result with score
4314
+ var result = { completion: true };
4315
+
4316
+ var raw = scormCourseData.scoreRaw;
4317
+ var max = scormCourseData.scoreMax || 100;
4318
+ var min = scormCourseData.scoreMin || 0;
4319
+
4320
+ if (typeof raw === 'number' && !isNaN(raw)) {
4321
+ result.score = {
4322
+ raw: raw,
4323
+ max: max,
4324
+ min: min,
4325
+ scaled: scormCourseData.scoreScaled != null ? scormCourseData.scoreScaled : (max > min ? (raw - min) / (max - min) : 0)
4326
+ };
4327
+ }
4328
+
4329
+ result.success = (status === 'passed');
4330
+
4331
+ var activityDetails = {
4332
+ courseTitle: courseTitle,
4333
+ completionStatus: status,
4334
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
4335
+ employeeId: LRS.employeeId || undefined
4336
+ };
4337
+
4338
+ // 1. Send "completed" statement (course was finished)
4339
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4340
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4341
+ sendStatement(completedStatement);
4342
+
4343
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4344
+ if (status === 'passed' || status === 'failed') {
4345
+ log('Sending', status, 'statement');
4346
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4347
+ sendStatement(outcomeStatement);
4348
+ }
4349
+
4350
+ logGroupEnd();
4351
+ }
4352
+
4169
4353
  function setupScormInteractionTracker() {
4170
4354
  if (!TRACK_QUIZZES) return;
4171
4355