Add SCORM API shim for proper content communication

SCORM content expects window.API (1.2) or window.API_1484_11 (2004)
on the parent frame. Without it, content fails to initialize.

Implements all 8 required methods for both SCORM versions:
- Initialize/LMSInitialize, Terminate/LMSFinish
- GetValue/LMSGetValue, SetValue/LMSSetValue
- Commit/LMSCommit, error methods
- Auto-marks lesson complete when content reports completion
- Removed iframe sandbox to allow parent frame API access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-04-11 01:36:44 +02:00
parent db98c6ae69
commit 84c899a814

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
@ -9,6 +9,85 @@ import 'katex/dist/katex.min.css'
import { useAuth } from '../context/AuthContext'
import api from '../api/client'
// SCORM Player with API shim
function ScormPlayer({ src, title, lessonId, courseId, onComplete }) {
const iframeRef = useRef(null)
const dataRef = useRef({})
const completedRef = useRef(false)
const setupApi = useCallback(() => {
const data = dataRef.current
// SCORM 1.2 API
const API = {
LMSInitialize: () => { return "true" },
LMSFinish: () => {
const status = data["cmi.core.lesson_status"]
if ((status === "completed" || status === "passed") && !completedRef.current) {
completedRef.current = true
onComplete?.()
}
return "true"
},
LMSGetValue: (key) => data[key] || "",
LMSSetValue: (key, val) => { data[key] = val; return "true" },
LMSCommit: () => {
const status = data["cmi.core.lesson_status"]
if ((status === "completed" || status === "passed") && !completedRef.current) {
completedRef.current = true
onComplete?.()
}
return "true"
},
LMSGetLastError: () => "0",
LMSGetErrorString: () => "No error",
LMSGetDiagnostic: () => "No diagnostic",
}
// SCORM 2004 API
const API_1484_11 = {
Initialize: () => "true",
Terminate: () => {
const status = data["cmi.completion_status"]
if ((status === "completed") && !completedRef.current) {
completedRef.current = true
onComplete?.()
}
return "true"
},
GetValue: (key) => data[key] || "",
SetValue: (key, val) => { data[key] = val; return "true" },
Commit: () => {
const status = data["cmi.completion_status"]
if ((status === "completed") && !completedRef.current) {
completedRef.current = true
onComplete?.()
}
return "true"
},
GetLastError: () => "0",
GetErrorString: () => "No error",
GetDiagnostic: () => "No diagnostic",
}
// Expose on window so iframe content can find it by walking up parent frames
window.API = API
window.API_1484_11 = API_1484_11
}, [onComplete])
useEffect(() => {
setupApi()
return () => { delete window.API; delete window.API_1484_11 }
}, [setupApi])
return (
<iframe
ref={iframeRef}
src={src}
style={{ width: '100%', height: 600, border: '1px solid var(--border)', borderRadius: 8 }}
title={title}
allow="fullscreen"
/>
)
}
const LESSON_ICONS = {
text: '\u{1F4C4}',
video: '\u{1F3AC}',
@ -475,12 +554,16 @@ export default function CourseDetailPage() {
{!loadingLesson && activeLesson.lesson_type === 'scorm_package' && (
<div>
{activeLesson.local_file_path ? (
<iframe
<ScormPlayer
src={activeLesson.local_file_path}
style={{ width: '100%', height: 600, border: '1px solid var(--border)', borderRadius: 8 }}
title={activeLesson.title}
allow="fullscreen"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
lessonId={activeLesson.id}
courseId={courseId}
onComplete={() => {
if (!completedLessons.has(activeLesson.id)) {
markComplete(activeLesson.id)
}
}}
/>
) : (
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: 20 }}>No SCORM package uploaded.</p>