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