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