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.
- package/README.md +8 -1
- package/bin/cli.js +16 -0
- package/framework/css/components/accordions.css +117 -31
- package/framework/css/layouts/article.css +7 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +4 -2
- package/framework/docs/FRAMEWORK_GUIDE.md +25 -7
- package/framework/docs/USER_GUIDE.md +14 -0
- package/framework/js/app/AppActions.js +17 -23
- package/framework/js/app/AppUI.js +7 -4
- package/framework/js/assessment/AssessmentActions.js +16 -15
- package/framework/js/assessment/AssessmentFactory.js +44 -15
- package/framework/js/automation/api-engagement.js +6 -3
- package/framework/js/components/interactions/drag-drop.js +4 -4
- package/framework/js/components/interactions/fill-in.js +7 -4
- package/framework/js/components/interactions/hotspot.js +2 -2
- package/framework/js/components/interactions/matching.js +4 -3
- package/framework/js/components/ui-components/audio-player.js +30 -8
- package/framework/js/components/ui-components/collapse.js +4 -2
- package/framework/js/components/ui-components/dropdown.js +7 -3
- package/framework/js/components/ui-components/embed-frame.js +5 -1
- package/framework/js/components/ui-components/flip-card.js +12 -6
- package/framework/js/components/ui-components/interactive-image.js +28 -52
- package/framework/js/components/ui-components/lightbox.js +19 -5
- package/framework/js/components/ui-components/modal.js +20 -7
- package/framework/js/components/ui-components/notifications.js +8 -3
- package/framework/js/components/ui-components/video-player.js +46 -20
- package/framework/js/core/event-bus.js +31 -28
- package/framework/js/dev/runtime-linter.js +6 -2
- package/framework/js/drivers/cmi5-driver.js +186 -83
- package/framework/js/drivers/driver-factory.js +7 -1
- package/framework/js/drivers/driver-interface.js +1 -1
- package/framework/js/drivers/http-driver-base.js +6 -0
- package/framework/js/drivers/lti-driver.js +112 -47
- package/framework/js/drivers/proxy-driver.js +347 -49
- package/framework/js/drivers/scorm-12-driver.js +213 -130
- package/framework/js/drivers/scorm-2004-driver.js +32 -19
- package/framework/js/drivers/scorm-driver-base.js +12 -0
- package/framework/js/drivers/standalone-driver.js +139 -0
- package/framework/js/engagement/engagement-trackers.js +38 -13
- package/framework/js/engagement/requirement-strategies.js +5 -1
- package/framework/js/main.js +7 -0
- package/framework/js/managers/assessment-manager.js +16 -1
- package/framework/js/managers/audio-manager.js +50 -16
- package/framework/js/managers/flag-manager.js +5 -1
- package/framework/js/managers/objective-manager.js +15 -7
- package/framework/js/managers/score-manager.js +27 -6
- package/framework/js/managers/video-manager.js +149 -70
- package/framework/js/navigation/NavigationActions.js +18 -4
- package/framework/js/navigation/document-gallery.js +7 -2
- package/framework/js/navigation/navigation-validators.js +3 -2
- package/framework/js/state/lms-connection.js +41 -16
- package/framework/js/state/state-commits.js +10 -5
- package/framework/js/state/state-manager.js +95 -9
- package/framework/js/state/state-validation.js +44 -11
- package/framework/js/utilities/course-helpers.js +3 -2
- package/framework/js/utilities/media-utils.js +28 -0
- package/framework/js/utilities/portable-assets.js +151 -0
- package/framework/js/utilities/ui-initializer.js +79 -5
- package/framework/js/utilities/utilities.js +32 -20
- package/framework/js/utilities/view-manager.js +16 -1
- package/framework/js/validation/scorm-validators.js +69 -0
- package/framework/version.json +2 -2
- package/lib/authoring-api.js +37 -18
- package/lib/build-linter.js +192 -2
- package/lib/build-packaging.js +47 -3
- package/lib/build.js +69 -8
- package/lib/cloud.js +40 -11
- package/lib/convert.js +30 -2
- package/lib/create.js +5 -3
- package/lib/dev.js +1 -1
- package/lib/headless-browser.js +66 -14
- package/lib/manifest/cmi5-manifest.js +26 -4
- package/lib/manifest/lti-tool-config.js +7 -2
- package/lib/manifest/manifest-factory.d.ts +1 -1
- package/lib/manifest/manifest-factory.js +3 -2
- package/lib/manifest/scorm-12-manifest.js +2 -2
- package/lib/manifest/scorm-2004-manifest.js +3 -28
- package/lib/manifest/scorm-proxy-manifest.js +37 -32
- package/lib/manifest/xml-utils.js +10 -0
- package/lib/narration.js +2 -2
- package/lib/portable-html.js +196 -0
- package/lib/preview-export.js +1 -1
- package/lib/preview-server.js +27 -6
- package/lib/project-utils.js +9 -1
- package/lib/proxy-templates/proxy.html +4 -1
- package/lib/proxy-templates/scorm-bridge.js +82 -10
- package/lib/scaffold.js +9 -4
- package/lib/stub-player/lms-api.js +63 -8
- package/package.json +5 -1
- package/schemas/adlcp_rootv1p2.xsd +110 -0
- package/schemas/coursecode_scorm12_package.xsd +5 -0
- package/schemas/coursecode_scorm2004_package.xsd +8 -0
- package/schemas/imscp_rootv1p1p2.xsd +304 -0
- package/template/course/slides/example-ui-showcase.js +1 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
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
|
|
429
|
-
return;
|
|
472
|
+
if (!this._agsProxyEndpoint) {
|
|
473
|
+
return false;
|
|
430
474
|
}
|
|
431
475
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
497
|
+
if (!response.ok) {
|
|
498
|
+
throw new Error(`AGS proxy rejected score: ${response.status} ${response.statusText || ''}`.trim());
|
|
499
|
+
}
|
|
455
500
|
|
|
456
|
-
|
|
501
|
+
logger.debug('[LtiDriver] Score posted to AGS:', this._score);
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
457
504
|
|
|
458
|
-
|
|
459
|
-
|
|
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
|
// =========================================================================
|