@patch-adams/core 1.4.2 → 1.4.5

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
@@ -720,48 +720,55 @@ function generateLrsBridgeCode(options) {
720
720
 
721
721
  function setupBridge() {
722
722
  try {
723
- // 1. Try PlayerIntegration first (Xyleme native mode)
723
+ // 1. Try PlayerIntegration first (Xyleme native mode) - for SCORM APIs
724
724
  var integrationResult = findPlayerIntegration(10);
725
725
  if (integrationResult) {
726
726
  LRS.integration = integrationResult.integration;
727
727
  LRS.integrationLevel = integrationResult.level;
728
- LRS.mode = 'playerIntegration';
729
- log('Using PlayerIntegration mode');
728
+ log('Found PlayerIntegration at level', integrationResult.level);
730
729
 
731
730
  if (LRS.integration.Internal_SCORM_2004_API) {
732
731
  window.API_1484_11 = LRS.integration.Internal_SCORM_2004_API;
733
732
  LRS.scormApi = window.API_1484_11;
733
+ LRS.scormApiFound = true;
734
+ LRS.scormApiType = '2004';
734
735
  }
735
736
  if (LRS.integration.Internal_SCORM_12_API) {
736
737
  window.API = LRS.integration.Internal_SCORM_12_API;
738
+ if (!LRS.scormApiFound) {
739
+ LRS.scormApi = window.API;
740
+ LRS.scormApiFound = true;
741
+ LRS.scormApiType = '1.2';
742
+ }
737
743
  }
738
- return true;
739
- }
740
-
741
- // 2. Search for SCORM APIs
742
- log('No PlayerIntegration found, searching for SCORM APIs...');
743
-
744
- var api2004 = findAPIInFrameHierarchy('API_1484_11', 10);
745
- if (api2004) {
746
- LRS.scormApi = api2004.api;
747
- LRS.scormApiLevel = api2004.level;
748
- LRS.scormApiWindow = api2004.window;
749
- LRS.scormApiFound = true;
750
- LRS.scormApiType = '2004';
751
- log('Found SCORM 2004 API at level ' + api2004.level);
744
+ // Don't return early - continue to detect LRS endpoint for direct xAPI
752
745
  } else {
753
- var api12 = findAPIInFrameHierarchy('API', 10);
754
- if (api12) {
755
- LRS.scormApi = api12.api;
756
- LRS.scormApiLevel = api12.level;
757
- LRS.scormApiWindow = api12.window;
746
+ // 2. Search for SCORM APIs directly
747
+ log('No PlayerIntegration found, searching for SCORM APIs...');
748
+
749
+ var api2004 = findAPIInFrameHierarchy('API_1484_11', 10);
750
+ if (api2004) {
751
+ LRS.scormApi = api2004.api;
752
+ LRS.scormApiLevel = api2004.level;
753
+ LRS.scormApiWindow = api2004.window;
758
754
  LRS.scormApiFound = true;
759
- LRS.scormApiType = '1.2';
760
- log('Found SCORM 1.2 API at level ' + api12.level);
755
+ LRS.scormApiType = '2004';
756
+ log('Found SCORM 2004 API at level ' + api2004.level);
757
+ } else {
758
+ var api12 = findAPIInFrameHierarchy('API', 10);
759
+ if (api12) {
760
+ LRS.scormApi = api12.api;
761
+ LRS.scormApiLevel = api12.level;
762
+ LRS.scormApiWindow = api12.window;
763
+ LRS.scormApiFound = true;
764
+ LRS.scormApiType = '1.2';
765
+ log('Found SCORM 1.2 API at level ' + api12.level);
766
+ }
761
767
  }
762
768
  }
763
769
 
764
- // 3. Determine LRS endpoint
770
+ // 3. ALWAYS try to determine LRS endpoint for direct xAPI statements
771
+ // This is critical for detailed tracking (media, interactions, quizzes)
765
772
  // Priority: 1) Configured endpoint, 2) Auto-detected Bravais endpoint
766
773
  if (LRS_ENDPOINT_CONFIG && LRS_ENDPOINT_CONFIG.length > 0) {
767
774
  LRS_ENDPOINT = LRS_ENDPOINT_CONFIG;
@@ -777,9 +784,17 @@ function generateLrsBridgeCode(options) {
777
784
  LRS.lrsEndpoint = LRS_ENDPOINT;
778
785
 
779
786
  // 4. Determine mode based on what we found
787
+ // Prefer directLRS when available for detailed tracking
780
788
  if (LRS_ENDPOINT && LRS_ENDPOINT.length > 0) {
781
789
  LRS.mode = 'directLRS';
782
790
  log('Using direct LRS mode with endpoint:', LRS_ENDPOINT);
791
+ if (LRS.integration) {
792
+ log('PlayerIntegration also available for SCORM state');
793
+ }
794
+ return true;
795
+ } else if (LRS.integration) {
796
+ LRS.mode = 'playerIntegration';
797
+ log('Using PlayerIntegration mode (no direct LRS endpoint)');
783
798
  return true;
784
799
  } else if (LRS.scormApiFound) {
785
800
  LRS.mode = 'scormOnly';
@@ -1998,6 +2013,154 @@ function generateLrsBridgeCode(options) {
1998
2013
  return info;
1999
2014
  }
2000
2015
 
2016
+ // ========================================================================
2017
+ // 4b. LESSON/SECTION NAME EXTRACTION
2018
+ // ========================================================================
2019
+
2020
+ /**
2021
+ * Get the current lesson/section information from Rise DOM
2022
+ * Returns { id, name, lessonIndex, sectionName }
2023
+ */
2024
+ function getCurrentLessonInfo() {
2025
+ var lessonInfo = {
2026
+ id: window.location.hash || '#/',
2027
+ name: null,
2028
+ lessonIndex: null,
2029
+ sectionName: null
2030
+ };
2031
+
2032
+ try {
2033
+ // 1. Try to get from Rise's navigation sidebar
2034
+ // Rise shows active lesson with specific classes
2035
+ var activeLesson = document.querySelector(
2036
+ '.sidebar__link--current, ' +
2037
+ '.sidebar-lesson--active, ' +
2038
+ '.nav-sidebar__lesson--active, ' +
2039
+ '[class*="sidebar"] [class*="current"], ' +
2040
+ '[class*="sidebar"] [class*="active"], ' +
2041
+ '[aria-current="page"], ' +
2042
+ '[aria-current="true"]'
2043
+ );
2044
+
2045
+ if (activeLesson) {
2046
+ var lessonTitle = activeLesson.querySelector(
2047
+ '.sidebar__link-text, ' +
2048
+ '.lesson-title, ' +
2049
+ '.sidebar-lesson__title, ' +
2050
+ '[class*="title"], ' +
2051
+ '[class*="label"]'
2052
+ );
2053
+ if (lessonTitle) {
2054
+ lessonInfo.name = lessonTitle.textContent.trim();
2055
+ } else {
2056
+ lessonInfo.name = activeLesson.textContent.trim();
2057
+ }
2058
+
2059
+ // Try to get the section/module name (parent group)
2060
+ var section = activeLesson.closest(
2061
+ '.sidebar__section, ' +
2062
+ '.sidebar-section, ' +
2063
+ '[class*="section"], ' +
2064
+ '[class*="module"]'
2065
+ );
2066
+ if (section) {
2067
+ var sectionTitle = section.querySelector(
2068
+ '.sidebar__section-title, ' +
2069
+ '.section-title, ' +
2070
+ '[class*="section-title"], ' +
2071
+ '[class*="module-title"], ' +
2072
+ 'h2, h3'
2073
+ );
2074
+ if (sectionTitle) {
2075
+ lessonInfo.sectionName = sectionTitle.textContent.trim();
2076
+ }
2077
+ }
2078
+ }
2079
+
2080
+ // 2. Try Rise's header/breadcrumb
2081
+ if (!lessonInfo.name) {
2082
+ var headerTitle = document.querySelector(
2083
+ '.lesson-header__title, ' +
2084
+ '.lesson__title, ' +
2085
+ '.content-header__title, ' +
2086
+ '[class*="lesson"] [class*="header"] [class*="title"], ' +
2087
+ '.blocks-lesson-header__title'
2088
+ );
2089
+ if (headerTitle) {
2090
+ lessonInfo.name = headerTitle.textContent.trim();
2091
+ }
2092
+ }
2093
+
2094
+ // 3. Try breadcrumbs
2095
+ if (!lessonInfo.name || !lessonInfo.sectionName) {
2096
+ var breadcrumbs = document.querySelectorAll(
2097
+ '.breadcrumb__item, ' +
2098
+ '.breadcrumbs li, ' +
2099
+ '[class*="breadcrumb"] span, ' +
2100
+ '[class*="breadcrumb"] a'
2101
+ );
2102
+ if (breadcrumbs.length >= 2) {
2103
+ // Usually: Course > Section > Lesson
2104
+ if (!lessonInfo.sectionName && breadcrumbs.length >= 2) {
2105
+ lessonInfo.sectionName = breadcrumbs[breadcrumbs.length - 2].textContent.trim();
2106
+ }
2107
+ if (!lessonInfo.name) {
2108
+ lessonInfo.name = breadcrumbs[breadcrumbs.length - 1].textContent.trim();
2109
+ }
2110
+ }
2111
+ }
2112
+
2113
+ // 4. Try Rise's internal data
2114
+ if (window.__RISE_COURSE_DATA__ && window.__RISE_COURSE_DATA__.lessons) {
2115
+ var lessonId = lessonInfo.id.replace('#/lessons/', '').replace('#/', '');
2116
+ var lessons = window.__RISE_COURSE_DATA__.lessons;
2117
+ for (var i = 0; i < lessons.length; i++) {
2118
+ if (lessons[i].id === lessonId || lessons[i].slug === lessonId) {
2119
+ lessonInfo.name = lessons[i].title || lessonInfo.name;
2120
+ lessonInfo.lessonIndex = i;
2121
+ break;
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ // 5. Clean up - remove any "Home" or generic names if we're on a real lesson
2127
+ if (lessonInfo.name) {
2128
+ lessonInfo.name = lessonInfo.name.replace(/^\\d+\\.\\s*/, ''); // Remove leading numbers "1. "
2129
+ if (lessonInfo.name.length > 200) {
2130
+ lessonInfo.name = lessonInfo.name.substring(0, 200) + '...';
2131
+ }
2132
+ }
2133
+
2134
+ if (lessonInfo.sectionName) {
2135
+ lessonInfo.sectionName = lessonInfo.sectionName.replace(/^\\d+\\.\\s*/, '');
2136
+ if (lessonInfo.sectionName.length > 200) {
2137
+ lessonInfo.sectionName = lessonInfo.sectionName.substring(0, 200) + '...';
2138
+ }
2139
+ }
2140
+
2141
+ } catch (e) {
2142
+ warn('Error extracting lesson info:', e);
2143
+ }
2144
+
2145
+ log('Current lesson info:', lessonInfo);
2146
+ return lessonInfo;
2147
+ }
2148
+
2149
+ /**
2150
+ * Cache the current lesson info (updates on navigation)
2151
+ */
2152
+ var cachedLessonInfo = null;
2153
+ var cachedLessonHash = null;
2154
+
2155
+ function getCachedLessonInfo() {
2156
+ var currentHash = window.location.hash;
2157
+ if (cachedLessonHash !== currentHash) {
2158
+ cachedLessonInfo = getCurrentLessonInfo();
2159
+ cachedLessonHash = currentHash;
2160
+ }
2161
+ return cachedLessonInfo || getCurrentLessonInfo();
2162
+ }
2163
+
2001
2164
  // ========================================================================
2002
2165
  // 5. XAPI STATEMENT BUILDING
2003
2166
  // ========================================================================
@@ -2342,14 +2505,20 @@ function generateLrsBridgeCode(options) {
2342
2505
  LRS.eventLog.push({
2343
2506
  statement: statement,
2344
2507
  timestamp: new Date().toISOString(),
2345
- mode: LRS.mode
2508
+ mode: LRS.mode,
2509
+ lrsEndpoint: LRS_ENDPOINT
2346
2510
  });
2347
2511
 
2348
- // Route based on mode
2349
- if (LRS.mode === 'playerIntegration' && LRS.integration) {
2350
- return sendViaPlayerIntegration(statement);
2351
- } else if (LRS.mode === 'directLRS' && LRS_ENDPOINT) {
2512
+ // ALWAYS prefer direct LRS when endpoint is available
2513
+ // This ensures all our detailed tracking (media, interactions, etc.) goes to the LRS
2514
+ // PlayerIntegration is used only for SCORM state, not for xAPI statements
2515
+ if (LRS_ENDPOINT && LRS_ENDPOINT.length > 0) {
2516
+ log('Sending via directLRS:', LRS_ENDPOINT);
2352
2517
  return sendViaDirectLRS(statement);
2518
+ } else if (LRS.mode === 'playerIntegration' && LRS.integration) {
2519
+ // Fallback to PlayerIntegration only if no direct LRS endpoint
2520
+ log('No LRS endpoint, falling back to PlayerIntegration');
2521
+ return sendViaPlayerIntegration(statement);
2353
2522
  } else {
2354
2523
  // SCORM-only or offline mode - just log
2355
2524
  log('Statement logged (no LRS):', statement.verb.display['en-US']);
@@ -2488,13 +2657,16 @@ function generateLrsBridgeCode(options) {
2488
2657
  LRS.contentOpened = function(data) {
2489
2658
  if (!TRACK_NAVIGATION) return;
2490
2659
 
2660
+ // Get current lesson info from DOM (includes name)
2661
+ var lessonInfo = getCurrentLessonInfo();
2662
+
2491
2663
  var result = null;
2492
2664
  if (data.duration) {
2493
2665
  result = { duration: data.duration };
2494
2666
  }
2495
2667
 
2496
2668
  // Extract clean page/lesson ID from Rise hash paths
2497
- var lessonId = data.pageGuid || data.lessonId || '';
2669
+ var lessonId = data.pageGuid || data.lessonId || lessonInfo.id || '';
2498
2670
  if (lessonId.indexOf('#/lessons/') === 0) {
2499
2671
  lessonId = lessonId.substring(10); // Remove '#/lessons/'
2500
2672
  } else if (lessonId.indexOf('#/') === 0) {
@@ -2505,7 +2677,9 @@ function generateLrsBridgeCode(options) {
2505
2677
  var additionalContext = {
2506
2678
  extensions: {
2507
2679
  pageId: lessonId || 'home',
2508
- pageTitle: data.title || 'Page',
2680
+ pageTitle: data.title || lessonInfo.name || 'Page',
2681
+ lessonName: lessonInfo.name,
2682
+ sectionName: lessonInfo.sectionName,
2509
2683
  pageUrl: data.url || window.location.href
2510
2684
  }
2511
2685
  };
@@ -2515,7 +2689,12 @@ function generateLrsBridgeCode(options) {
2515
2689
  var statement = buildStatement(
2516
2690
  'experienced',
2517
2691
  'http://xyleme.com/bravais/activities/document',
2518
- { event: 'page_viewed', pageId: lessonId },
2692
+ {
2693
+ event: 'page_viewed',
2694
+ pageId: lessonId,
2695
+ lessonName: lessonInfo.name,
2696
+ sectionName: lessonInfo.sectionName
2697
+ },
2519
2698
  result,
2520
2699
  additionalContext
2521
2700
  );
@@ -2526,6 +2705,9 @@ function generateLrsBridgeCode(options) {
2526
2705
  LRS.mediaPlayed = function(data) {
2527
2706
  if (!TRACK_MEDIA) return;
2528
2707
 
2708
+ // Get current lesson context
2709
+ var lessonInfo = getCachedLessonInfo();
2710
+
2529
2711
  var mediaType = data.type === 'audio' ? ACTIVITY_TYPES.audio : ACTIVITY_TYPES.video;
2530
2712
  var verbKey = data.action === 'play' ? 'played' :
2531
2713
  data.action === 'pause' ? 'paused' :
@@ -2536,7 +2718,11 @@ function generateLrsBridgeCode(options) {
2536
2718
  mediaSrc: data.src || 'media-' + Date.now(),
2537
2719
  mediaName: data.name || (data.type || 'Media'),
2538
2720
  mediaType: data.type || 'video',
2539
- 'https://w3id.org/xapi/video/extensions/length': data.duration || 0
2721
+ 'https://w3id.org/xapi/video/extensions/length': data.duration || 0,
2722
+ // Include lesson/section context
2723
+ lessonId: lessonInfo.id,
2724
+ lessonName: lessonInfo.name,
2725
+ sectionName: lessonInfo.sectionName
2540
2726
  };
2541
2727
 
2542
2728
  var result = {
@@ -2559,12 +2745,21 @@ function generateLrsBridgeCode(options) {
2559
2745
  LRS.assessmentStarted = function(data) {
2560
2746
  if (!TRACK_QUIZZES) return;
2561
2747
 
2748
+ // Get lesson context
2749
+ var lessonInfo = getCachedLessonInfo();
2750
+
2562
2751
  // Build assessment activity object for Xyleme format
2563
2752
  var assessmentObject = buildAssessmentActivityObject({
2564
2753
  assessmentGuid: data.assessmentGuid || data.id,
2565
- title: data.title || 'Assessment'
2754
+ title: data.title || data.assessmentName || 'Assessment'
2566
2755
  });
2567
2756
 
2757
+ // Add lesson context to extensions
2758
+ assessmentObject.definition = assessmentObject.definition || {};
2759
+ assessmentObject.definition.extensions = assessmentObject.definition.extensions || {};
2760
+ assessmentObject.definition.extensions.lessonName = lessonInfo.name;
2761
+ assessmentObject.definition.extensions.sectionName = lessonInfo.sectionName;
2762
+
2568
2763
  var statement = buildStatementXyleme('attempted', assessmentObject);
2569
2764
  sendStatement(statement);
2570
2765
  };
@@ -2573,13 +2768,31 @@ function generateLrsBridgeCode(options) {
2573
2768
  LRS.assessmentEnded = function(data) {
2574
2769
  if (!TRACK_QUIZZES) return;
2575
2770
 
2771
+ // Get lesson context (may be passed in from extractQuizResult)
2772
+ var lessonInfo = data.lessonName ? { name: data.lessonName, sectionName: data.sectionName } : getCachedLessonInfo();
2773
+
2576
2774
  // Build assessment activity object for Xyleme format
2577
2775
  var assessmentObject = buildAssessmentActivityObject({
2578
- assessmentGuid: data.assessmentGuid || data.id,
2579
- title: data.title || 'Assessment'
2776
+ assessmentGuid: data.assessmentGuid || data.assessmentId || data.id,
2777
+ title: data.title || data.assessmentName || 'Assessment'
2580
2778
  });
2581
2779
 
2582
- var result = { completion: true };
2780
+ // Add lesson context to extensions
2781
+ assessmentObject.definition = assessmentObject.definition || {};
2782
+ assessmentObject.definition.extensions = assessmentObject.definition.extensions || {};
2783
+ assessmentObject.definition.extensions.lessonName = lessonInfo.name;
2784
+ assessmentObject.definition.extensions.sectionName = lessonInfo.sectionName;
2785
+ assessmentObject.definition.extensions.assessmentName = data.assessmentName || data.title;
2786
+
2787
+ var result = {
2788
+ completion: true,
2789
+ extensions: {
2790
+ 'https://patch-adams.io/xapi/extensions/assessmentName': data.assessmentName || data.title || null,
2791
+ 'https://patch-adams.io/xapi/extensions/lessonName': lessonInfo.name || null,
2792
+ 'https://patch-adams.io/xapi/extensions/sectionName': lessonInfo.sectionName || null,
2793
+ 'https://patch-adams.io/xapi/extensions/questionCount': data.questionCount || null
2794
+ }
2795
+ };
2583
2796
 
2584
2797
  if (typeof data.score === 'number') {
2585
2798
  result.score = {
@@ -2608,14 +2821,27 @@ function generateLrsBridgeCode(options) {
2608
2821
  LRS.questionAnswered = function(data) {
2609
2822
  if (!TRACK_QUIZZES) return;
2610
2823
 
2824
+ // Get lesson context if not provided
2825
+ var lessonInfo = data.lessonName ? data : getCachedLessonInfo();
2826
+
2611
2827
  // Build question activity object for Xyleme format
2612
2828
  var questionObject = buildQuestionActivityObject({
2613
2829
  questionGuid: data.questionGuid || data.questionId,
2614
2830
  text: data.questionText || 'Question'
2615
2831
  });
2616
2832
 
2833
+ // Build comprehensive result with human-readable data
2617
2834
  var result = {
2618
- response: data.answer || data.response || ''
2835
+ response: data.answer || data.response || '',
2836
+ extensions: {
2837
+ 'https://patch-adams.io/xapi/extensions/questionNumber': data.questionNumber || null,
2838
+ 'https://patch-adams.io/xapi/extensions/questionText': data.questionText || null,
2839
+ 'https://patch-adams.io/xapi/extensions/answerText': data.answer || data.response || null,
2840
+ 'https://patch-adams.io/xapi/extensions/correctAnswer': data.correctAnswer || null,
2841
+ 'https://patch-adams.io/xapi/extensions/assessmentName': data.assessmentName || null,
2842
+ 'https://patch-adams.io/xapi/extensions/lessonName': lessonInfo.lessonName || lessonInfo.name || null,
2843
+ 'https://patch-adams.io/xapi/extensions/sectionName': lessonInfo.sectionName || null
2844
+ }
2619
2845
  };
2620
2846
 
2621
2847
  if (data.result === 'correct' || data.correct === true) {
@@ -2647,6 +2873,9 @@ function generateLrsBridgeCode(options) {
2647
2873
  LRS.interacted = function(data) {
2648
2874
  if (!TRACK_INTERACTIONS) return;
2649
2875
 
2876
+ // Get current lesson context
2877
+ var lessonInfo = getCachedLessonInfo();
2878
+
2650
2879
  // Activity type is interaction, details include interaction ID and type
2651
2880
  var statement = buildStatement(
2652
2881
  'interacted',
@@ -2654,7 +2883,11 @@ function generateLrsBridgeCode(options) {
2654
2883
  {
2655
2884
  interactionId: data.id || 'interaction-' + Date.now(),
2656
2885
  interactionName: data.name || data.type || 'Interaction',
2657
- interactionType: data.interactionType || data.type || 'other'
2886
+ interactionType: data.interactionType || data.type || 'other',
2887
+ // Include lesson/section context
2888
+ lessonId: lessonInfo.id,
2889
+ lessonName: lessonInfo.name,
2890
+ sectionName: lessonInfo.sectionName
2658
2891
  }
2659
2892
  );
2660
2893
  sendStatement(statement);
@@ -2735,49 +2968,146 @@ function generateLrsBridgeCode(options) {
2735
2968
  function setupMediaInterceptors() {
2736
2969
  if (!TRACK_MEDIA) return;
2737
2970
 
2738
- document.addEventListener('play', function(e) {
2739
- var target = e.target;
2740
- if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
2971
+ // Track which elements we've already attached listeners to
2972
+ var trackedMedia = new WeakSet();
2973
+
2974
+ // Helper to get media name/title from element or surrounding context
2975
+ function getMediaName(el) {
2976
+ // Check element attributes
2977
+ var name = el.title || el.getAttribute('aria-label') || el.getAttribute('data-name');
2978
+ if (name) return name;
2979
+
2980
+ // Check parent Rise video block for title
2981
+ var videoBlock = findClosest(el, '[data-block-type="video"], .blocks-video, .video-block');
2982
+ if (videoBlock) {
2983
+ var titleEl = videoBlock.querySelector('.video-title, .block-title, [class*="title"]');
2984
+ if (titleEl) return titleEl.textContent.trim();
2985
+ }
2986
+
2987
+ // Use src filename as fallback
2988
+ var src = el.src || el.currentSrc || '';
2989
+ if (src) {
2990
+ var filename = src.split('/').pop().split('?')[0];
2991
+ if (filename && filename.length < 100) return filename;
2992
+ }
2993
+
2994
+ return el.tagName.toLowerCase();
2995
+ }
2996
+
2997
+ // Attach media event listeners to a single element
2998
+ function attachMediaListeners(el) {
2999
+ if (trackedMedia.has(el)) return;
3000
+ trackedMedia.add(el);
3001
+
3002
+ var mediaType = el.tagName.toLowerCase();
3003
+ log('Attaching media listeners to:', mediaType, el.src || el.currentSrc || 'no-src');
3004
+
3005
+ el.addEventListener('play', function() {
3006
+ log('Media play event captured:', mediaType);
2741
3007
  LRS.mediaPlayed({
2742
- type: target.tagName.toLowerCase(),
2743
- src: target.src || target.currentSrc || '',
2744
- name: target.title || target.getAttribute('aria-label') || '',
2745
- currentTime: target.currentTime || 0,
2746
- duration: target.duration || 0,
3008
+ type: mediaType,
3009
+ src: el.src || el.currentSrc || '',
3010
+ name: getMediaName(el),
3011
+ currentTime: el.currentTime || 0,
3012
+ duration: el.duration || 0,
2747
3013
  action: 'play'
2748
3014
  });
2749
- }
2750
- }, true);
3015
+ });
2751
3016
 
2752
- document.addEventListener('pause', function(e) {
2753
- var target = e.target;
2754
- if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3017
+ el.addEventListener('pause', function() {
3018
+ // Ignore pause events that fire right before ended
3019
+ if (el.ended) return;
3020
+ log('Media pause event captured:', mediaType);
2755
3021
  LRS.mediaPlayed({
2756
- type: target.tagName.toLowerCase(),
2757
- src: target.src || target.currentSrc || '',
2758
- name: target.title || target.getAttribute('aria-label') || '',
2759
- currentTime: target.currentTime || 0,
2760
- duration: target.duration || 0,
3022
+ type: mediaType,
3023
+ src: el.src || el.currentSrc || '',
3024
+ name: getMediaName(el),
3025
+ currentTime: el.currentTime || 0,
3026
+ duration: el.duration || 0,
2761
3027
  action: 'pause'
2762
3028
  });
2763
- }
2764
- }, true);
3029
+ });
2765
3030
 
2766
- document.addEventListener('ended', function(e) {
2767
- var target = e.target;
2768
- if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3031
+ el.addEventListener('ended', function() {
3032
+ log('Media ended event captured:', mediaType);
2769
3033
  LRS.mediaPlayed({
2770
- type: target.tagName.toLowerCase(),
2771
- src: target.src || target.currentSrc || '',
2772
- name: target.title || target.getAttribute('aria-label') || '',
2773
- currentTime: target.duration || 0,
2774
- duration: target.duration || 0,
3034
+ type: mediaType,
3035
+ src: el.src || el.currentSrc || '',
3036
+ name: getMediaName(el),
3037
+ currentTime: el.duration || 0,
3038
+ duration: el.duration || 0,
2775
3039
  action: 'completed'
2776
3040
  });
3041
+ });
3042
+ }
3043
+
3044
+ // Scan for and attach listeners to all video/audio elements
3045
+ function scanForMedia(root) {
3046
+ var elements = (root || document).querySelectorAll('video, audio');
3047
+ log('Scanning for media elements, found:', elements.length);
3048
+ for (var i = 0; i < elements.length; i++) {
3049
+ attachMediaListeners(elements[i]);
3050
+ }
3051
+ }
3052
+
3053
+ // Initial scan
3054
+ scanForMedia();
3055
+
3056
+ // Document-level capture for any we might miss
3057
+ document.addEventListener('play', function(e) {
3058
+ var target = e.target;
3059
+ if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
3060
+ // Attach listeners if not already tracked
3061
+ if (!trackedMedia.has(target)) {
3062
+ log('Captured play from untracked media, attaching listeners');
3063
+ attachMediaListeners(target);
3064
+ }
2777
3065
  }
2778
3066
  }, true);
2779
3067
 
2780
- log('Media interceptors set up');
3068
+ // MutationObserver to detect dynamically added video/audio elements
3069
+ var mediaObserver = new MutationObserver(function(mutations) {
3070
+ var needsScan = false;
3071
+ mutations.forEach(function(mutation) {
3072
+ mutation.addedNodes.forEach(function(node) {
3073
+ if (node.nodeType === 1) {
3074
+ if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') {
3075
+ attachMediaListeners(node);
3076
+ } else if (node.querySelectorAll) {
3077
+ // Check for nested video/audio
3078
+ var nested = node.querySelectorAll('video, audio');
3079
+ if (nested.length > 0) {
3080
+ needsScan = true;
3081
+ }
3082
+ }
3083
+ }
3084
+ });
3085
+ });
3086
+ if (needsScan) {
3087
+ scanForMedia();
3088
+ }
3089
+ });
3090
+
3091
+ // Start observing once DOM is ready
3092
+ function startMediaObserver() {
3093
+ if (document.body) {
3094
+ mediaObserver.observe(document.body, {
3095
+ childList: true,
3096
+ subtree: true
3097
+ });
3098
+ log('Media mutation observer started');
3099
+ } else {
3100
+ setTimeout(startMediaObserver, 100);
3101
+ }
3102
+ }
3103
+ startMediaObserver();
3104
+
3105
+ // Periodic rescan for Rise lazy-loaded content
3106
+ setInterval(function() {
3107
+ scanForMedia();
3108
+ }, 3000);
3109
+
3110
+ log('Media interceptors set up (enhanced)');
2781
3111
  }
2782
3112
 
2783
3113
  function setupNavigationInterceptors() {
@@ -2858,43 +3188,152 @@ function generateLrsBridgeCode(options) {
2858
3188
  'quiz-' + Date.now();
2859
3189
  }
2860
3190
 
3191
+ /**
3192
+ * Extract quiz/assessment name from the DOM
3193
+ */
3194
+ function extractAssessmentName(node) {
3195
+ // Try various selectors for assessment/quiz titles
3196
+ var titleEl = node.querySelector(
3197
+ '.quiz-title, .assessment-title, .knowledge-check-title, ' +
3198
+ '.blocks-quiz__title, .block-title, ' +
3199
+ '[class*="quiz"] [class*="title"], ' +
3200
+ '[class*="assessment"] [class*="title"], ' +
3201
+ 'h2, h3'
3202
+ );
3203
+ if (titleEl) {
3204
+ return titleEl.textContent.trim().substring(0, 200);
3205
+ }
3206
+
3207
+ // Try to get from parent block
3208
+ var parentBlock = node.closest('[data-block-type]');
3209
+ if (parentBlock) {
3210
+ var blockTitle = parentBlock.querySelector('.block-title, h2, h3');
3211
+ if (blockTitle) {
3212
+ return blockTitle.textContent.trim().substring(0, 200);
3213
+ }
3214
+ }
3215
+
3216
+ // Get current lesson name as context
3217
+ var lessonInfo = getCachedLessonInfo();
3218
+ if (lessonInfo.name) {
3219
+ return 'Quiz in ' + lessonInfo.name;
3220
+ }
3221
+
3222
+ return 'Knowledge Check';
3223
+ }
3224
+
2861
3225
  function extractQuizResult(node) {
2862
3226
  var scoreEl = node.querySelector('.score-value, .quiz-score, .score-percentage, [class*="score"]');
2863
3227
  var score = scoreEl ? parseFloat(scoreEl.textContent.replace(/[^0-9.]/g, '')) : null;
2864
3228
 
2865
- var questions = node.querySelectorAll('.question-result, .question-feedback, .question-item, [class*="question"]');
3229
+ // Get assessment name and lesson context
3230
+ var assessmentName = extractAssessmentName(node);
3231
+ var lessonInfo = getCachedLessonInfo();
3232
+
3233
+ // Find all question containers - Rise uses various structures
3234
+ var questions = node.querySelectorAll(
3235
+ '.question-result, .question-feedback, .question-item, ' +
3236
+ '.blocks-quiz__question, .quiz-question, ' +
3237
+ '[class*="question-"][class*="result"], ' +
3238
+ '[class*="question-"][class*="feedback"], ' +
3239
+ '[data-question-id]'
3240
+ );
3241
+
3242
+ log('Found', questions.length, 'question elements in quiz result');
2866
3243
 
2867
3244
  if (questions.length > 0) {
2868
3245
  questions.forEach(function(q, index) {
3246
+ // Determine if correct - check multiple indicators
2869
3247
  var isCorrect = q.classList.contains('correct') ||
2870
- q.querySelector('.correct, [class*="correct"]') !== null ||
2871
- q.getAttribute('data-correct') === 'true';
3248
+ q.classList.contains('is-correct') ||
3249
+ q.querySelector('.correct, .is-correct, [class*="correct"]') !== null ||
3250
+ q.getAttribute('data-correct') === 'true' ||
3251
+ q.getAttribute('data-result') === 'correct';
2872
3252
 
3253
+ // Get the question text - try multiple selectors
2873
3254
  var questionText = '';
2874
- var questionEl = q.querySelector('.question-text, .question-stem, .question-title, [class*="question-text"]');
3255
+ var questionEl = q.querySelector(
3256
+ '.question-text, .question-stem, .question-title, ' +
3257
+ '.blocks-quiz__question-text, .quiz-question__text, ' +
3258
+ '[class*="question-text"], [class*="question-stem"], ' +
3259
+ '[class*="prompt"], p:first-of-type'
3260
+ );
2875
3261
  if (questionEl) {
2876
3262
  questionText = questionEl.textContent.trim();
2877
3263
  }
2878
3264
 
3265
+ // Get the learner's selected answer text
2879
3266
  var answerText = '';
2880
- var answerEl = q.querySelector('.selected-answer, .learner-response, .answer-text, [class*="response"]');
3267
+ var answerEl = q.querySelector(
3268
+ '.selected-answer, .learner-response, .answer-text, ' +
3269
+ '.user-answer, .chosen-answer, .response-text, ' +
3270
+ '.blocks-quiz__response, .quiz-question__response, ' +
3271
+ '[class*="selected"], [class*="chosen"], [class*="response"], ' +
3272
+ '[class*="user-answer"]'
3273
+ );
2881
3274
  if (answerEl) {
2882
3275
  answerText = answerEl.textContent.trim();
2883
3276
  }
2884
3277
 
3278
+ // If no specific answer element, try to find selected radio/checkbox label
3279
+ if (!answerText) {
3280
+ var selectedInput = q.querySelector('input:checked, [aria-checked="true"]');
3281
+ if (selectedInput) {
3282
+ var label = q.querySelector('label[for="' + selectedInput.id + '"]');
3283
+ if (label) {
3284
+ answerText = label.textContent.trim();
3285
+ } else {
3286
+ var parentLabel = selectedInput.closest('label');
3287
+ if (parentLabel) {
3288
+ answerText = parentLabel.textContent.trim();
3289
+ }
3290
+ }
3291
+ }
3292
+ }
3293
+
3294
+ // Get question ID if available
3295
+ var questionId = q.getAttribute('data-question-id') ||
3296
+ q.getAttribute('data-block-id') ||
3297
+ q.getAttribute('id') ||
3298
+ 'q' + (index + 1);
3299
+
3300
+ // Get correct answer text if available (for reporting)
3301
+ var correctAnswerText = '';
3302
+ var correctEl = q.querySelector(
3303
+ '.correct-answer, .right-answer, ' +
3304
+ '[class*="correct-answer"], [class*="right-answer"]'
3305
+ );
3306
+ if (correctEl) {
3307
+ correctAnswerText = correctEl.textContent.trim();
3308
+ }
3309
+
3310
+ log('Question', index + 1, ':', {
3311
+ questionText: questionText.substring(0, 50) + '...',
3312
+ answerText: answerText.substring(0, 50) + '...',
3313
+ isCorrect: isCorrect
3314
+ });
3315
+
2885
3316
  LRS.questionAnswered({
2886
- questionId: 'q' + (index + 1),
2887
- questionText: questionText.substring(0, 200),
2888
- answer: answerText.substring(0, 200),
2889
- result: isCorrect ? 'correct' : 'incorrect'
3317
+ questionId: questionId,
3318
+ questionNumber: index + 1,
3319
+ questionText: questionText.substring(0, 500),
3320
+ answer: answerText.substring(0, 500),
3321
+ correctAnswer: correctAnswerText.substring(0, 500),
3322
+ result: isCorrect ? 'correct' : 'incorrect',
3323
+ assessmentName: assessmentName,
3324
+ lessonName: lessonInfo.name,
3325
+ sectionName: lessonInfo.sectionName
2890
3326
  });
2891
3327
  });
2892
3328
  }
2893
3329
 
2894
3330
  LRS.assessmentEnded({
2895
3331
  assessmentId: extractAssessmentId(node),
3332
+ assessmentName: assessmentName,
2896
3333
  score: score,
2897
- questionCount: questions.length
3334
+ questionCount: questions.length,
3335
+ lessonName: lessonInfo.name,
3336
+ sectionName: lessonInfo.sectionName
2898
3337
  });
2899
3338
  }
2900
3339
 
@@ -3018,7 +3457,7 @@ function generateLrsBridgeCode(options) {
3018
3457
  // ========================================================================
3019
3458
 
3020
3459
  function init() {
3021
- log('Initializing LRS bridge v2.2.0...');
3460
+ log('Initializing LRS bridge v2.5.0...');
3022
3461
 
3023
3462
  // Extract course info early
3024
3463
  extractCourseInfo();