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:
parent
db98c6ae69
commit
84c899a814
1 changed files with 88 additions and 5 deletions
|
|
@ -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 { useParams, useNavigate, Link } from 'react-router-dom'
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
|
|
@ -9,6 +9,85 @@ import 'katex/dist/katex.min.css'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import api from '../api/client'
|
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 = {
|
const LESSON_ICONS = {
|
||||||
text: '\u{1F4C4}',
|
text: '\u{1F4C4}',
|
||||||
video: '\u{1F3AC}',
|
video: '\u{1F3AC}',
|
||||||
|
|
@ -475,12 +554,16 @@ export default function CourseDetailPage() {
|
||||||
{!loadingLesson && activeLesson.lesson_type === 'scorm_package' && (
|
{!loadingLesson && activeLesson.lesson_type === 'scorm_package' && (
|
||||||
<div>
|
<div>
|
||||||
{activeLesson.local_file_path ? (
|
{activeLesson.local_file_path ? (
|
||||||
<iframe
|
<ScormPlayer
|
||||||
src={activeLesson.local_file_path}
|
src={activeLesson.local_file_path}
|
||||||
style={{ width: '100%', height: 600, border: '1px solid var(--border)', borderRadius: 8 }}
|
|
||||||
title={activeLesson.title}
|
title={activeLesson.title}
|
||||||
allow="fullscreen"
|
lessonId={activeLesson.id}
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
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>
|
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: 20 }}>No SCORM package uploaded.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue