coursecode 0.1.57 → 0.1.59

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.
Files changed (95) hide show
  1. package/README.md +8 -1
  2. package/bin/cli.js +16 -0
  3. package/framework/css/components/accordions.css +117 -31
  4. package/framework/css/layouts/article.css +7 -0
  5. package/framework/docs/COURSE_AUTHORING_GUIDE.md +4 -2
  6. package/framework/docs/FRAMEWORK_GUIDE.md +25 -7
  7. package/framework/docs/USER_GUIDE.md +14 -0
  8. package/framework/js/app/AppActions.js +17 -23
  9. package/framework/js/app/AppUI.js +7 -4
  10. package/framework/js/assessment/AssessmentActions.js +16 -15
  11. package/framework/js/assessment/AssessmentFactory.js +44 -15
  12. package/framework/js/automation/api-engagement.js +6 -3
  13. package/framework/js/components/interactions/drag-drop.js +4 -4
  14. package/framework/js/components/interactions/fill-in.js +7 -4
  15. package/framework/js/components/interactions/hotspot.js +2 -2
  16. package/framework/js/components/interactions/matching.js +4 -3
  17. package/framework/js/components/ui-components/audio-player.js +30 -8
  18. package/framework/js/components/ui-components/collapse.js +4 -2
  19. package/framework/js/components/ui-components/dropdown.js +7 -3
  20. package/framework/js/components/ui-components/embed-frame.js +5 -1
  21. package/framework/js/components/ui-components/flip-card.js +12 -6
  22. package/framework/js/components/ui-components/interactive-image.js +28 -52
  23. package/framework/js/components/ui-components/lightbox.js +19 -5
  24. package/framework/js/components/ui-components/modal.js +20 -7
  25. package/framework/js/components/ui-components/notifications.js +8 -3
  26. package/framework/js/components/ui-components/video-player.js +46 -20
  27. package/framework/js/core/event-bus.js +31 -28
  28. package/framework/js/dev/runtime-linter.js +6 -2
  29. package/framework/js/drivers/cmi5-driver.js +186 -83
  30. package/framework/js/drivers/driver-factory.js +7 -1
  31. package/framework/js/drivers/driver-interface.js +1 -1
  32. package/framework/js/drivers/http-driver-base.js +6 -0
  33. package/framework/js/drivers/lti-driver.js +112 -47
  34. package/framework/js/drivers/proxy-driver.js +347 -49
  35. package/framework/js/drivers/scorm-12-driver.js +213 -130
  36. package/framework/js/drivers/scorm-2004-driver.js +32 -19
  37. package/framework/js/drivers/scorm-driver-base.js +12 -0
  38. package/framework/js/drivers/standalone-driver.js +139 -0
  39. package/framework/js/engagement/engagement-trackers.js +38 -13
  40. package/framework/js/engagement/requirement-strategies.js +5 -1
  41. package/framework/js/main.js +7 -0
  42. package/framework/js/managers/assessment-manager.js +16 -1
  43. package/framework/js/managers/audio-manager.js +50 -16
  44. package/framework/js/managers/flag-manager.js +5 -1
  45. package/framework/js/managers/objective-manager.js +15 -7
  46. package/framework/js/managers/score-manager.js +27 -6
  47. package/framework/js/managers/video-manager.js +149 -70
  48. package/framework/js/navigation/NavigationActions.js +18 -4
  49. package/framework/js/navigation/document-gallery.js +7 -2
  50. package/framework/js/navigation/navigation-validators.js +3 -2
  51. package/framework/js/state/lms-connection.js +41 -16
  52. package/framework/js/state/state-commits.js +10 -5
  53. package/framework/js/state/state-manager.js +95 -9
  54. package/framework/js/state/state-validation.js +44 -11
  55. package/framework/js/utilities/course-helpers.js +3 -2
  56. package/framework/js/utilities/media-utils.js +28 -0
  57. package/framework/js/utilities/portable-assets.js +151 -0
  58. package/framework/js/utilities/ui-initializer.js +79 -5
  59. package/framework/js/utilities/utilities.js +32 -20
  60. package/framework/js/utilities/view-manager.js +16 -1
  61. package/framework/js/validation/scorm-validators.js +69 -0
  62. package/framework/version.json +2 -2
  63. package/lib/authoring-api.js +37 -18
  64. package/lib/build-linter.js +192 -2
  65. package/lib/build-packaging.js +47 -3
  66. package/lib/build.js +69 -8
  67. package/lib/cloud.js +40 -11
  68. package/lib/convert.js +30 -2
  69. package/lib/create.js +5 -3
  70. package/lib/dev.js +1 -1
  71. package/lib/headless-browser.js +66 -14
  72. package/lib/manifest/cmi5-manifest.js +26 -4
  73. package/lib/manifest/lti-tool-config.js +7 -2
  74. package/lib/manifest/manifest-factory.d.ts +1 -1
  75. package/lib/manifest/manifest-factory.js +3 -2
  76. package/lib/manifest/scorm-12-manifest.js +2 -2
  77. package/lib/manifest/scorm-2004-manifest.js +3 -28
  78. package/lib/manifest/scorm-proxy-manifest.js +37 -32
  79. package/lib/manifest/xml-utils.js +10 -0
  80. package/lib/narration.js +2 -2
  81. package/lib/portable-html.js +196 -0
  82. package/lib/preview-export.js +1 -1
  83. package/lib/preview-server.js +27 -6
  84. package/lib/project-utils.js +9 -1
  85. package/lib/proxy-templates/proxy.html +4 -1
  86. package/lib/proxy-templates/scorm-bridge.js +82 -10
  87. package/lib/scaffold.js +9 -4
  88. package/lib/stub-player/lms-api.js +63 -8
  89. package/package.json +5 -1
  90. package/schemas/adlcp_rootv1p2.xsd +110 -0
  91. package/schemas/coursecode_scorm12_package.xsd +5 -0
  92. package/schemas/coursecode_scorm2004_package.xsd +8 -0
  93. package/schemas/imscp_rootv1p1p2.xsd +304 -0
  94. package/template/course/slides/example-ui-showcase.js +1 -1
  95. package/template/vite.config.js +50 -22
@@ -35,6 +35,8 @@ export class LtiDriver extends HttpDriverBase {
35
35
 
36
36
  // Host state endpoint for suspend_data persistence
37
37
  this._stateEndpoint = null;
38
+ this._gradeDirty = false;
39
+ this._gradeFingerprint = null;
38
40
  }
39
41
 
40
42
  // =========================================================================
@@ -131,7 +133,7 @@ export class LtiDriver extends HttpDriverBase {
131
133
 
132
134
  try {
133
135
  await this._persistState();
134
- await this._postScore();
136
+ await this._flushGrade();
135
137
 
136
138
  this._isTerminated = true;
137
139
  logger.debug('[LtiDriver] Session terminated');
@@ -139,11 +141,32 @@ export class LtiDriver extends HttpDriverBase {
139
141
 
140
142
  } catch (error) {
141
143
  logger.error('[LtiDriver] Terminate failed:', error);
142
- this._isTerminated = true;
143
144
  throw new Error(`[LtiDriver] Termination failed: ${error.message}`);
144
145
  }
145
146
  }
146
147
 
148
+ async commit() {
149
+ const result = await super.commit();
150
+ if (!result || this._mock) return result;
151
+ await this._flushGrade();
152
+ return true;
153
+ }
154
+
155
+ reportScore(score) {
156
+ super.reportScore(score);
157
+ this._gradeDirty = true;
158
+ }
159
+
160
+ reportCompletion(status) {
161
+ super.reportCompletion(status);
162
+ this._gradeDirty = true;
163
+ }
164
+
165
+ reportSuccess(status) {
166
+ super.reportSuccess(status);
167
+ this._gradeDirty = true;
168
+ }
169
+
147
170
  /**
148
171
  * Emergency save using sendBeacon for page unload scenarios.
149
172
  */
@@ -186,7 +209,9 @@ export class LtiDriver extends HttpDriverBase {
186
209
  location: this._bookmarkCache,
187
210
  completionStatus: this._completionStatus,
188
211
  successStatus: this._successStatus,
189
- score: this._score
212
+ score: this._score,
213
+ gradePending: this._gradeDirty,
214
+ gradeFingerprint: this._currentGradeFingerprint()
190
215
  }
191
216
  };
192
217
  const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
@@ -350,7 +375,13 @@ export class LtiDriver extends HttpDriverBase {
350
375
  _getStateKey() {
351
376
  if (this._claims) {
352
377
  const resourceLink = this._claims['https://purl.imsglobal.org/spec/lti/claim/resource_link']?.id;
353
- return `${resourceLink}:${this._claims.sub}`;
378
+ const deploymentId = this._claims['https://purl.imsglobal.org/spec/lti/claim/deployment_id'];
379
+ return JSON.stringify({
380
+ issuer: this._claims.iss,
381
+ deploymentId,
382
+ resourceLinkId: resourceLink,
383
+ userId: this._claims.sub
384
+ });
354
385
  }
355
386
  return null;
356
387
  }
@@ -365,23 +396,34 @@ export class LtiDriver extends HttpDriverBase {
365
396
  const stateKey = this._getStateKey();
366
397
  if (!stateKey) return;
367
398
 
368
- try {
369
- const response = await fetch(`${this._stateEndpoint}?key=${encodeURIComponent(stateKey)}`, {
370
- credentials: 'same-origin'
371
- });
399
+ const response = await fetch(`${this._stateEndpoint}?key=${encodeURIComponent(stateKey)}`, {
400
+ credentials: 'same-origin'
401
+ });
402
+ if (response.status === 404) return;
403
+ if (!response.ok) {
404
+ throw new Error(`State prefetch failed: ${response.status} ${response.statusText || ''}`.trim());
405
+ }
372
406
 
373
- if (response.ok) {
374
- const state = await response.json();
375
- this._suspendDataCache = state.suspendData || null;
376
- this._bookmarkCache = state.bookmark || null;
377
- this._completionStatus = state.completionStatus || 'unknown';
378
- this._successStatus = state.successStatus || 'unknown';
379
- this._score = state.score ?? null;
380
- logger.debug('[LtiDriver] State pre-fetched');
381
- }
382
- } catch (error) {
383
- logger.warn('[LtiDriver] Failed to prefetch state:', error.message);
407
+ const state = await response.json();
408
+ this._suspendDataCache = state.suspendData || null;
409
+ this._bookmarkCache = state.bookmark || null;
410
+ this._completionStatus = state.completionStatus || 'unknown';
411
+ this._successStatus = state.successStatus || 'unknown';
412
+ this._score = state.score ?? null;
413
+ this._gradeFingerprint = state.gradeFingerprint || null;
414
+ if (typeof state.gradePending === 'boolean') {
415
+ this._gradeDirty = state.gradePending;
416
+ } else if (this._agsProxyEndpoint && (
417
+ this._score !== null ||
418
+ this._completionStatus === 'completed' ||
419
+ this._successStatus !== 'unknown'
420
+ )) {
421
+ // Upgrade recovery for launches saved before the persistent grade
422
+ // outbox existed. Re-sending an idempotent latest score is safer
423
+ // than permanently missing the grade.
424
+ this._gradeDirty = true;
384
425
  }
426
+ logger.debug('[LtiDriver] State pre-fetched');
385
427
  }
386
428
 
387
429
  async _persistState() {
@@ -390,7 +432,7 @@ export class LtiDriver extends HttpDriverBase {
390
432
  const stateKey = this._getStateKey();
391
433
  if (!stateKey) return;
392
434
 
393
- const dirty = this._suspendDataDirty || this._bookmarkDirty;
435
+ const dirty = this._suspendDataDirty || this._bookmarkDirty || this._gradeDirty;
394
436
  if (!dirty) return;
395
437
 
396
438
  const payload = {
@@ -399,7 +441,9 @@ export class LtiDriver extends HttpDriverBase {
399
441
  bookmark: this._bookmarkCache,
400
442
  completionStatus: this._completionStatus,
401
443
  successStatus: this._successStatus,
402
- score: this._score
444
+ score: this._score,
445
+ gradePending: this._gradeDirty,
446
+ gradeFingerprint: this._currentGradeFingerprint()
403
447
  };
404
448
 
405
449
  const response = await fetch(this._stateEndpoint, {
@@ -425,39 +469,60 @@ export class LtiDriver extends HttpDriverBase {
425
469
  // =========================================================================
426
470
 
427
471
  async _postScore() {
428
- if (!this._agsProxyEndpoint || this._score === null) {
429
- return;
472
+ if (!this._agsProxyEndpoint) {
473
+ return false;
430
474
  }
431
475
 
432
- try {
433
- const scorePayload = {
434
- userId: this._claims.sub,
435
- scoreGiven: this._score * 100,
436
- scoreMaximum: 100,
437
- comment: '',
438
- timestamp: new Date().toISOString(),
439
- activityProgress: this._completionStatus === 'completed' ? 'Completed' : 'InProgress',
440
- gradingProgress: this._successStatus !== 'unknown' ? 'FullyGraded' : 'NotReady'
441
- };
476
+ const scorePayload = {
477
+ userId: this._claims.sub,
478
+ comment: '',
479
+ timestamp: new Date().toISOString(),
480
+ activityProgress: this._completionStatus === 'completed' ? 'Completed' : 'InProgress',
481
+ gradingProgress: this._successStatus !== 'unknown' ? 'FullyGraded' : 'NotReady'
482
+ };
483
+ if (this._score !== null) {
484
+ scorePayload.scoreGiven = this._score * 100;
485
+ scorePayload.scoreMaximum = 100;
486
+ }
442
487
 
443
- const response = await fetch(this._agsProxyEndpoint, {
444
- method: 'POST',
445
- headers: {
446
- 'Content-Type': 'application/vnd.ims.lis.v1.score+json'
447
- },
448
- credentials: 'same-origin',
449
- body: JSON.stringify(scorePayload)
450
- });
488
+ const response = await fetch(this._agsProxyEndpoint, {
489
+ method: 'POST',
490
+ headers: {
491
+ 'Content-Type': 'application/vnd.ims.lis.v1.score+json'
492
+ },
493
+ credentials: 'same-origin',
494
+ body: JSON.stringify(scorePayload)
495
+ });
451
496
 
452
- if (!response.ok) {
453
- throw new Error(`AGS proxy rejected score: ${response.status} ${response.statusText || ''}`.trim());
454
- }
497
+ if (!response.ok) {
498
+ throw new Error(`AGS proxy rejected score: ${response.status} ${response.statusText || ''}`.trim());
499
+ }
455
500
 
456
- logger.debug('[LtiDriver] Score posted to AGS:', this._score);
501
+ logger.debug('[LtiDriver] Score posted to AGS:', this._score);
502
+ return true;
503
+ }
457
504
 
458
- } catch (error) {
459
- logger.error('[LtiDriver] Failed to post score to AGS:', error.message);
505
+ async _flushGrade() {
506
+ if (!this._gradeDirty) return;
507
+ if (!this._agsProxyEndpoint) {
508
+ this._gradeDirty = false;
509
+ return;
460
510
  }
511
+ await this._postScore();
512
+ this._gradeDirty = false;
513
+ this._gradeFingerprint = this._currentGradeFingerprint();
514
+ // Acknowledge the outbox only after AGS accepts the score. If this
515
+ // persistence fails, the next launch safely retries the latest grade.
516
+ this._bookmarkDirty = true;
517
+ await this._persistState();
518
+ }
519
+
520
+ _currentGradeFingerprint() {
521
+ return JSON.stringify({
522
+ score: this._score,
523
+ completionStatus: this._completionStatus,
524
+ successStatus: this._successStatus
525
+ });
461
526
  }
462
527
 
463
528
  // =========================================================================