coursecode 0.1.0

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.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,423 @@
1
+ import {
2
+ validateAgainstSchema,
3
+ createInteractionEventHandler,
4
+ renderInteractionControls,
5
+ renderFeedbackContainer,
6
+ displayFeedback,
7
+ clearFeedback,
8
+ normalizeInitialResponse,
9
+ validateContainer,
10
+ registerCoreInteraction
11
+ } from './interaction-base.js';
12
+
13
+ // Metadata for sequencing interaction type
14
+ export const metadata = {
15
+ creator: 'createSequencingQuestion',
16
+ scormType: 'sequencing',
17
+ showCheckAnswer: true,
18
+ isAnswered: (response) => {
19
+ return Array.isArray(response) && response.length > 0;
20
+ },
21
+ getCorrectAnswer: (config) => {
22
+ return JSON.stringify(config.correctOrder || []);
23
+ },
24
+ formatCorrectAnswer: (question, correctAnswer) => {
25
+ let html = '<ol class="list-decimal pl-4 m-0">';
26
+ const correctOrder = Array.isArray(correctAnswer) ? correctAnswer : [];
27
+ const items = question.items || [];
28
+
29
+ correctOrder.forEach(itemId => {
30
+ const item = items.find(i => i.id === itemId);
31
+ if (item) {
32
+ html += `<li>${item.text}</li>`;
33
+ }
34
+ });
35
+ html += '</ol>';
36
+ return html;
37
+ },
38
+ formatUserResponse: (question, response) => {
39
+ let html = '<ol class="list-decimal pl-4 m-0">';
40
+ const userOrder = Array.isArray(response) ? response : [];
41
+ const items = question.items || [];
42
+
43
+ userOrder.forEach(itemId => {
44
+ const item = items.find(i => i.id === itemId);
45
+ if (item) {
46
+ html += `<li>${item.text}</li>`;
47
+ }
48
+ });
49
+ html += '</ol>';
50
+ return html;
51
+ }
52
+ };
53
+
54
+ // Schema for validation, linting, and AI-assisted authoring
55
+ export const schema = {
56
+ type: 'sequencing',
57
+ description: 'Drag-to-reorder items into correct sequence',
58
+ scormType: 'sequencing',
59
+ example: `<div class="interaction sequencing" data-interaction-id="demo-seq">
60
+ <div class="question-prompt"><h3>Arrange these steps in order:</h3></div>
61
+ <div class="sequencing-layout">
62
+ <div class="sequence-track" aria-hidden="true"><span class="sequence-label sequence-label-start">First</span><div class="sequence-track-line"></div><span class="sequence-label sequence-label-end">Last</span></div>
63
+ <div class="sequencing-list" role="list">
64
+ <div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Design</span></div>
65
+ <div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Develop</span></div>
66
+ <div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Test</span></div>
67
+ <div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Deploy</span></div>
68
+ </div>
69
+ </div>
70
+ <div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
71
+ </div>`,
72
+ properties: {
73
+ items: {
74
+ type: 'array',
75
+ required: true,
76
+ minItems: 2,
77
+ description: 'Items to sequence',
78
+ itemSchema: {
79
+ id: { type: 'string', required: true },
80
+ text: { type: 'string', required: true }
81
+ }
82
+ },
83
+ correctOrder: {
84
+ type: 'array',
85
+ required: true,
86
+ description: 'Array of item IDs in correct order'
87
+ }
88
+ }
89
+ };
90
+
91
+ export function createSequencingQuestion(config) {
92
+ validateAgainstSchema(config, schema);
93
+
94
+ const { id, prompt, items, correctOrder, controlled = false, sequenceLabels = null } = config;
95
+
96
+ // Validate items and correctOrder
97
+ if (!Array.isArray(items) || items.length < 2) {
98
+ throw new Error(`Sequencing question "${id}" must have at least two items`);
99
+ }
100
+ if (!Array.isArray(correctOrder) || correctOrder.length !== items.length) {
101
+ throw new Error(`Sequencing question "${id}" correctOrder length must match items length`);
102
+ }
103
+
104
+ let _container = null;
105
+ let _currentOrder = [];
106
+
107
+ const questionObj = {
108
+ id,
109
+ type: 'sequencing',
110
+
111
+ render: (container, initialResponse = null) => {
112
+ validateContainer(container, id);
113
+ _container = container;
114
+
115
+ // Determine initial order: use saved response or default (shuffled or as provided)
116
+ // For now, we'll use the order provided in 'items' as the initial display order.
117
+ // Ideally, we should shuffle them if no initial response exists.
118
+ let displayOrderIds = [];
119
+
120
+ const initialValue = normalizeInitialResponse(initialResponse);
121
+ if (Array.isArray(initialValue) && initialValue.length === items.length) {
122
+ displayOrderIds = initialValue;
123
+ } else {
124
+ // Default to items order (author should provide them shuffled or we shuffle here)
125
+ // Let's shuffle by default to ensure it's a challenge
126
+ displayOrderIds = items.map(i => i.id).sort(() => Math.random() - 0.5);
127
+ }
128
+
129
+ _currentOrder = [...displayOrderIds];
130
+
131
+ // Always show a direction cue; use provided labels or fall back to First/Last
132
+ const effectiveSequenceLabels = (sequenceLabels && Array.isArray(sequenceLabels) && sequenceLabels.length >= 2)
133
+ ? sequenceLabels
134
+ : ['First', 'Last'];
135
+
136
+ // Build sequence track with start label at top, line in middle, end label at bottom
137
+ const startLabel = effectiveSequenceLabels[0];
138
+ const endLabel = effectiveSequenceLabels[effectiveSequenceLabels.length - 1];
139
+ const sequenceTrackHtml = `
140
+ <div class="sequence-track" aria-hidden="true">
141
+ <span class="sequence-label sequence-label-start">${startLabel}</span>
142
+ <div class="sequence-track-line"></div>
143
+ <span class="sequence-label sequence-label-end">${endLabel}</span>
144
+ </div>
145
+ `;
146
+
147
+ let html = `
148
+ <div class="interaction sequencing" data-interaction-id="${id}">
149
+ <div class="question-prompt">
150
+ <h3>${prompt}</h3>
151
+ </div>
152
+ <div class="sequencing-layout">
153
+ ${sequenceTrackHtml}
154
+ <div class="sequencing-list" role="list">
155
+ `;
156
+
157
+ displayOrderIds.forEach((itemId, _index) => {
158
+ const item = items.find(i => i.id === itemId);
159
+ if (item) {
160
+ html += `
161
+ <div class="sequence-item"
162
+ draggable="true"
163
+ data-item-id="${item.id}"
164
+ role="listitem"
165
+ tabindex="0"
166
+ aria-label="${item.text}. Press Up or Down arrow to reorder.">
167
+ <span class="item-text">${item.text}</span>
168
+ </div>
169
+ `;
170
+ }
171
+ });
172
+
173
+ html += `
174
+ </div>
175
+ </div>
176
+ ${renderFeedbackContainer(id)}
177
+ ${renderInteractionControls(id, controlled)}
178
+ </div>
179
+ `;
180
+
181
+ container.innerHTML = html;
182
+
183
+ // Attach event listeners
184
+ const listContainer = container.querySelector('.sequencing-list');
185
+ attachDragAndDropListeners(listContainer);
186
+ attachKeyboardListeners(listContainer);
187
+
188
+ // Attach standard interaction handlers
189
+ container.addEventListener('click', createInteractionEventHandler(questionObj, config));
190
+ },
191
+
192
+ getResponse: () => {
193
+ return [..._currentOrder];
194
+ },
195
+
196
+ evaluate: (response) => {
197
+ const orderToCheck = Array.isArray(response) ? response : _currentOrder;
198
+ const isCorrect = JSON.stringify(orderToCheck) === JSON.stringify(correctOrder);
199
+
200
+ return {
201
+ correct: isCorrect,
202
+ score: isCorrect ? 1 : 0,
203
+ response: orderToCheck
204
+ };
205
+ },
206
+
207
+ setResponse: (response) => {
208
+ if (!Array.isArray(response)) return;
209
+ _currentOrder = [...response];
210
+ // Re-render to reflect new order
211
+ questionObj.render(_container, _currentOrder);
212
+ },
213
+
214
+ checkAnswer: () => {
215
+ const evaluation = questionObj.evaluate(_currentOrder);
216
+ const feedbackMsg = evaluation.correct
217
+ ? (config.feedback?.correct || 'Correct sequence!')
218
+ : (config.feedback?.incorrect || 'Incorrect sequence. Try again.');
219
+
220
+ displayFeedback(_container, id, feedbackMsg, evaluation.correct ? 'correct' : 'incorrect');
221
+ return { ...evaluation, feedback: feedbackMsg };
222
+ },
223
+
224
+ reset: () => {
225
+ clearFeedback(_container);
226
+ // Re-shuffle or reset to initial state
227
+ // For simplicity, we'll just re-render with a new shuffle
228
+ questionObj.render(_container, null);
229
+ },
230
+
231
+ showHint: () => {
232
+ // Optional: Highlight the first incorrect item
233
+ },
234
+
235
+ getCorrectAnswer: () => {
236
+ return [...correctOrder];
237
+ }
238
+ };
239
+
240
+ // For uncontrolled interactions, register with the central registry for lifecycle mgmt
241
+ if (!controlled) {
242
+ registerCoreInteraction(config, questionObj);
243
+ }
244
+
245
+ // Helper to update internal order state based on DOM
246
+ function updateOrderFromDOM(listContainer) {
247
+ const itemElements = Array.from(listContainer.querySelectorAll('.sequence-item'));
248
+ _currentOrder = itemElements.map(el => el.dataset.itemId);
249
+ }
250
+
251
+ function attachDragAndDropListeners(list) {
252
+ let draggedItem = null;
253
+
254
+ list.addEventListener('dragstart', (e) => {
255
+ draggedItem = e.target.closest('.sequence-item');
256
+ if (!draggedItem) {
257
+ e.preventDefault();
258
+ return;
259
+ }
260
+ e.dataTransfer.effectAllowed = 'move';
261
+ e.dataTransfer.setData('text/plain', draggedItem.dataset.itemId);
262
+ draggedItem.classList.add('dragging');
263
+ // Accessibility
264
+ draggedItem.setAttribute('aria-grabbed', 'true');
265
+ });
266
+
267
+ list.addEventListener('dragend', (_e) => {
268
+ if (draggedItem) {
269
+ draggedItem.classList.remove('dragging');
270
+ draggedItem.setAttribute('aria-grabbed', 'false');
271
+ draggedItem = null;
272
+ }
273
+ // Remove all drop indicators
274
+ list.querySelectorAll('.sequence-item').forEach(item => {
275
+ item.classList.remove('drag-over-top', 'drag-over-bottom');
276
+ });
277
+
278
+ updateOrderFromDOM(list);
279
+ });
280
+
281
+ // Required for drop to work - must prevent default on dragenter
282
+ list.addEventListener('dragenter', (e) => {
283
+ e.preventDefault();
284
+ e.dataTransfer.dropEffect = 'move';
285
+ });
286
+
287
+ list.addEventListener('dragover', (e) => {
288
+ e.preventDefault(); // Allow drop
289
+ e.dataTransfer.dropEffect = 'move'; // Show move cursor, not 🚫
290
+
291
+ // If no valid dragged item, skip indicator logic
292
+ if (!draggedItem) return;
293
+
294
+ // Clear all previous indicators
295
+ list.querySelectorAll('.sequence-item').forEach(item => {
296
+ item.classList.remove('drag-over-top', 'drag-over-bottom');
297
+ });
298
+
299
+ const items = Array.from(list.querySelectorAll('.sequence-item'));
300
+ if (items.length === 0) return;
301
+
302
+ // Find the item we should indicate drop position for
303
+ let targetItem = null;
304
+ let position = 'bottom'; // 'top' or 'bottom'
305
+
306
+ for (let i = 0; i < items.length; i++) {
307
+ const item = items[i];
308
+ if (item === draggedItem) continue;
309
+
310
+ const rect = item.getBoundingClientRect();
311
+ const midY = rect.top + rect.height / 2;
312
+
313
+ if (e.clientY < midY) {
314
+ targetItem = item;
315
+ position = 'top';
316
+ break;
317
+ } else {
318
+ targetItem = item;
319
+ position = 'bottom';
320
+ }
321
+ }
322
+
323
+ if (targetItem && targetItem !== draggedItem) {
324
+ targetItem.classList.add(position === 'top' ? 'drag-over-top' : 'drag-over-bottom');
325
+ }
326
+ });
327
+
328
+ list.addEventListener('dragleave', (e) => {
329
+ // Only clear if leaving the list entirely
330
+ if (!list.contains(e.relatedTarget)) {
331
+ list.querySelectorAll('.sequence-item').forEach(item => {
332
+ item.classList.remove('drag-over-top', 'drag-over-bottom');
333
+ });
334
+ }
335
+ });
336
+
337
+ list.addEventListener('drop', (e) => {
338
+ e.preventDefault();
339
+
340
+ const items = Array.from(list.querySelectorAll('.sequence-item'));
341
+ let targetItem = null;
342
+ let position = 'bottom';
343
+
344
+ for (let i = 0; i < items.length; i++) {
345
+ const item = items[i];
346
+ if (item === draggedItem) continue;
347
+
348
+ const rect = item.getBoundingClientRect();
349
+ const midY = rect.top + rect.height / 2;
350
+
351
+ if (e.clientY < midY) {
352
+ targetItem = item;
353
+ position = 'top';
354
+ break;
355
+ } else {
356
+ targetItem = item;
357
+ position = 'bottom';
358
+ }
359
+ }
360
+
361
+ if (targetItem && draggedItem && targetItem !== draggedItem) {
362
+ // Capture reference before it gets nullified in dragend
363
+ const droppedItem = draggedItem;
364
+
365
+ if (position === 'top') {
366
+ list.insertBefore(droppedItem, targetItem);
367
+ } else {
368
+ list.insertBefore(droppedItem, targetItem.nextSibling);
369
+ }
370
+
371
+ // Add settling animation to the dropped item
372
+ droppedItem.classList.add('settling');
373
+ droppedItem.addEventListener('animationend', () => {
374
+ droppedItem.classList.remove('settling');
375
+ }, { once: true });
376
+
377
+ targetItem.classList.remove('drag-over-top', 'drag-over-bottom');
378
+ updateOrderFromDOM(list);
379
+ }
380
+ });
381
+ }
382
+
383
+ function attachKeyboardListeners(list) {
384
+ list.addEventListener('keydown', (e) => {
385
+ const item = e.target.closest('.sequence-item');
386
+ if (!item) return;
387
+
388
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
389
+ e.preventDefault();
390
+
391
+ if (e.key === 'ArrowUp') {
392
+ const prev = item.previousElementSibling;
393
+ if (prev) {
394
+ // Capture reference for animationend callback
395
+ const movedItem = item;
396
+ list.insertBefore(movedItem, prev);
397
+ movedItem.classList.add('settling');
398
+ movedItem.addEventListener('animationend', () => {
399
+ movedItem.classList.remove('settling');
400
+ }, { once: true });
401
+ movedItem.focus();
402
+ updateOrderFromDOM(list);
403
+ }
404
+ } else if (e.key === 'ArrowDown') {
405
+ const next = item.nextElementSibling;
406
+ if (next) {
407
+ // Capture reference for animationend callback
408
+ const movedItem = item;
409
+ list.insertBefore(movedItem, next.nextSibling);
410
+ movedItem.classList.add('settling');
411
+ movedItem.addEventListener('animationend', () => {
412
+ movedItem.classList.remove('settling');
413
+ }, { once: true });
414
+ movedItem.focus();
415
+ updateOrderFromDOM(list);
416
+ }
417
+ }
418
+ }
419
+ });
420
+ }
421
+
422
+ return questionObj;
423
+ }
@@ -0,0 +1,241 @@
1
+ import {
2
+ validateAgainstSchema,
3
+ createInteractionEventHandler,
4
+ renderInteractionControls,
5
+ renderFeedbackContainer,
6
+ displayFeedback,
7
+ clearFeedback,
8
+ normalizeInitialResponse,
9
+ validateContainer,
10
+ registerCoreInteraction
11
+ } from './interaction-base.js';
12
+ import engagementManager from '../../engagement/engagement-manager.js';
13
+ import * as NavigationState from '../../navigation/NavigationState.js';
14
+
15
+ // Metadata for true-false interaction type
16
+ export const metadata = {
17
+ creator: 'createTrueFalseQuestion',
18
+ scormType: 'true-false',
19
+ showCheckAnswer: true,
20
+ isAnswered: (response) => {
21
+ return response !== null && response !== undefined;
22
+ },
23
+ getCorrectAnswer: (config) => config.correctAnswer.toString(),
24
+ formatCorrectAnswer: (question, correctAnswer) => `<p class="correct-item">${correctAnswer}</p>`,
25
+ formatUserResponse: (question, response) => `<p class="response-item">${response}</p>`
26
+ };
27
+
28
+ // Schema for validation, linting, and AI-assisted authoring
29
+ export const schema = {
30
+ type: 'true-false',
31
+ description: 'Binary true/false question with radio buttons',
32
+ scormType: 'true-false',
33
+ example: `<div class="interaction true-false" data-interaction-id="demo-tf">
34
+ <div class="question-prompt text-center"><h3>The Earth revolves around the Sun.</h3></div>
35
+ <div class="true-false-options">
36
+ <label><input type="radio" name="demo-tf" value="true"> <span class="tf-label">True</span></label>
37
+ <label><input type="radio" name="demo-tf" value="false"> <span class="tf-label">False</span></label>
38
+ </div>
39
+ <div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
40
+ </div>`,
41
+ properties: {
42
+ correctAnswer: {
43
+ type: 'boolean',
44
+ required: true,
45
+ description: 'The correct answer (true or false)'
46
+ },
47
+ autoCheck: {
48
+ type: 'boolean',
49
+ default: false,
50
+ description: 'Auto-evaluate on selection'
51
+ }
52
+ }
53
+ };
54
+
55
+ export function createTrueFalseQuestion(config) {
56
+ // Validate config on creation
57
+ validateAgainstSchema(config, schema);
58
+
59
+ const { id, prompt, correctAnswer, controlled = false, autoCheck = false, feedback = {} } = config;
60
+
61
+ let _container = null;
62
+
63
+ const questionObj = {
64
+ id,
65
+ type: 'true-false',
66
+
67
+ render: (container, initialResponse = null) => {
68
+ validateContainer(container, id);
69
+ _container = container;
70
+
71
+ const initialValue = normalizeInitialResponse(initialResponse);
72
+
73
+ const html = `
74
+ <div class="interaction true-false" data-interaction-id="${id}">
75
+ <div class="question-prompt text-center">
76
+ <h3>${prompt}</h3>
77
+ </div>
78
+ <div class="true-false-options">
79
+ <label>
80
+ <input type="radio" name="${id}_choice" value="true" ${initialValue === 'true' ? 'checked' : ''} data-testid="${id}-choice-true">
81
+ <span class="tf-label">True</span>
82
+ </label>
83
+ <label>
84
+ <input type="radio" name="${id}_choice" value="false" ${initialValue === 'false' ? 'checked' : ''} data-testid="${id}-choice-false">
85
+ <span class="tf-label">False</span>
86
+ </label>
87
+ </div>
88
+ ${autoCheck ? '' : renderInteractionControls(id, controlled)}
89
+ ${renderFeedbackContainer(id)}
90
+ </div>
91
+ `;
92
+
93
+ container.innerHTML = html;
94
+
95
+ // Attach event handler only in uncontrolled mode
96
+ if (!controlled) {
97
+ container.addEventListener('click', createInteractionEventHandler(questionObj, {
98
+ ...config,
99
+ scormType: 'true-false',
100
+ correctPattern: correctAnswer.toString()
101
+ }));
102
+ }
103
+
104
+ // Auto-check on radio selection if enabled
105
+ if (autoCheck && !controlled) {
106
+ const radios = container.querySelectorAll(`input[name="${id}_choice"]`);
107
+ radios.forEach(radio => {
108
+ radio.addEventListener('change', () => {
109
+ const evaluation = questionObj.checkAnswer();
110
+
111
+ // Track engagement when autoCheck triggers evaluation
112
+ if (evaluation) {
113
+ const currentSlideId = NavigationState.getCurrentSlideId();
114
+ if (currentSlideId) {
115
+ engagementManager.trackInteraction(
116
+ currentSlideId,
117
+ id,
118
+ true, // completed
119
+ evaluation.correct
120
+ );
121
+ }
122
+ }
123
+
124
+ if (evaluation && !evaluation.correct) {
125
+ // Flash incorrect feedback, then reset after 2 seconds
126
+ setTimeout(() => {
127
+ questionObj.reset();
128
+ }, 2000);
129
+ }
130
+ // Correct answers stay green (don't reset)
131
+ });
132
+ });
133
+ }
134
+ },
135
+
136
+ evaluate: (response) => {
137
+ if (response === null || response === undefined || response === '') {
138
+ return {
139
+ score: 0,
140
+ correct: false,
141
+ response: '',
142
+ error: 'No answer provided'
143
+ };
144
+ }
145
+
146
+ const correct = response === correctAnswer.toString();
147
+ return {
148
+ score: correct ? 1 : 0,
149
+ correct,
150
+ response
151
+ };
152
+ },
153
+
154
+ checkAnswer: () => {
155
+ validateContainer(_container, id);
156
+
157
+ const selected = _container.querySelector(`input[name="${id}_choice"]:checked`);
158
+
159
+ if (!selected) {
160
+ displayFeedback(_container, id, 'Please select an answer.', 'error');
161
+ return null;
162
+ }
163
+
164
+ const response = selected.value;
165
+ const evaluation = questionObj.evaluate(response);
166
+
167
+ // Get the selected label for visual feedback
168
+ const selectedLabel = selected.closest('label');
169
+
170
+ if (evaluation.correct) {
171
+ const message = feedback.correct || `Correct! The answer is ${correctAnswer}.`;
172
+ displayFeedback(_container, id, message, 'correct');
173
+ if (selectedLabel) {
174
+ selectedLabel.classList.add('answer-correct');
175
+ selectedLabel.classList.remove('answer-incorrect');
176
+ }
177
+ } else {
178
+ const message = feedback.incorrect || `Incorrect. The correct answer is ${correctAnswer}.`;
179
+ displayFeedback(_container, id, message, 'incorrect');
180
+ if (selectedLabel) {
181
+ selectedLabel.classList.add('answer-incorrect');
182
+ selectedLabel.classList.remove('answer-correct');
183
+ }
184
+ }
185
+
186
+ return evaluation;
187
+ },
188
+
189
+ reset: () => {
190
+ validateContainer(_container, id);
191
+
192
+ const radios = _container.querySelectorAll(`input[name="${id}_choice"]`);
193
+ const labels = _container.querySelectorAll('.true-false-options label');
194
+
195
+ radios.forEach(radio => {
196
+ radio.checked = false;
197
+ });
198
+
199
+ // Clear answer state classes from labels
200
+ labels.forEach(label => {
201
+ label.classList.remove('answer-correct', 'answer-incorrect');
202
+ });
203
+
204
+ clearFeedback(_container, id);
205
+ },
206
+
207
+ setResponse: (response) => {
208
+ validateContainer(_container, id);
209
+
210
+ if (response === null || response === undefined) {
211
+ return;
212
+ }
213
+
214
+ const radio = _container.querySelector(`input[name="${id}_choice"][value="${response}"]`);
215
+ if (!radio) {
216
+ throw new Error(`Invalid response value "${response}" for true-false question "${id}". Must be "true" or "false".`);
217
+ }
218
+
219
+ radio.checked = true;
220
+ },
221
+
222
+ getResponse: () => {
223
+ validateContainer(_container, id);
224
+
225
+ const selected = _container.querySelector(`input[name="${id}_choice"]:checked`);
226
+ return selected ? selected.value : null;
227
+ },
228
+
229
+ getCorrectAnswer: () => {
230
+ return correctAnswer.toString();
231
+ }
232
+ };
233
+
234
+ // For uncontrolled interactions, register with the central registry for lifecycle mgmt
235
+ if (!controlled) {
236
+ registerCoreInteraction(config, questionObj);
237
+ }
238
+
239
+ return questionObj;
240
+ }
241
+