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,400 @@
1
+ /**
2
+ * stub-player/edit-utils.js - Shared editing utilities
3
+ *
4
+ * Provides reusable edit form rendering and save logic for both
5
+ * standalone interactions and assessment questions.
6
+ */
7
+
8
+ /**
9
+ * Escape HTML entities for safe rendering in form values
10
+ */
11
+ export function escapeHtml(str) {
12
+ if (!str) return '';
13
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
14
+ }
15
+
16
+ /**
17
+ * Render an edit form for an interaction or question
18
+ * Now schema-aware when a schema is provided
19
+ * @param {Object} item - The interaction or question object with type
20
+ * @param {Object|null} schema - Optional schema for the interaction type
21
+ * @returns {string} HTML string for the edit form
22
+ */
23
+ export function renderEditForm(item, schema = null) {
24
+ let html = '<div class="edit-form">';
25
+
26
+ // If no schema provided, fall back to legacy hardcoded rendering
27
+ if (!schema) {
28
+ return renderLegacyEditForm(item);
29
+ }
30
+
31
+ const props = schema.properties || {};
32
+ // schema.extends contains base properties directly (not nested under .properties)
33
+ const baseProps = schema.extends || {};
34
+ const allProps = { ...baseProps, ...props };
35
+
36
+ // Priority order for common fields
37
+ const priorityFields = ['label', 'prompt', 'correctAnswer'];
38
+ const sortedKeys = [
39
+ ...priorityFields.filter(k => k in allProps),
40
+ ...Object.keys(allProps).filter(k => !priorityFields.includes(k) && !['id'].includes(k))
41
+ ];
42
+
43
+ for (const propName of sortedKeys) {
44
+ const propDef = allProps[propName];
45
+ // Handle zones/dropZones naming mismatch between schema and data
46
+ let value = item[propName];
47
+ if (propName === 'dropZones' && value === undefined) {
48
+ value = item['zones']; // Fallback to zones if dropZones not found
49
+ }
50
+ const isPresent = value !== undefined && value !== null;
51
+
52
+ // Skip non-editable or complex nested types for now
53
+ if (propDef.type === 'object') continue;
54
+
55
+ const requiredIndicator = propDef.required ? '<span class="required-indicator" title="Required">*</span>' : '';
56
+ const tooltip = propDef.description ? ` title="${escapeHtml(propDef.description)}"` : '';
57
+ const label = formatLabel(propName);
58
+
59
+ html += `<div class="edit-field"${tooltip}>`;
60
+ html += `<label>${label}${requiredIndicator}</label>`;
61
+
62
+ // Render appropriate input based on type
63
+ if (propDef.type === 'boolean') {
64
+ const checked = value === true || value === 'true';
65
+ html += `<select name="${propName}">
66
+ <option value="true" ${checked ? 'selected' : ''}>True</option>
67
+ <option value="false" ${!checked ? 'selected' : ''}>False</option>
68
+ </select>`;
69
+ } else if (propDef.enum) {
70
+ html += `<select name="${propName}">`;
71
+ for (const opt of propDef.enum) {
72
+ html += `<option value="${escapeHtml(opt)}" ${value === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`;
73
+ }
74
+ html += '</select>';
75
+ } else if (propDef.type === 'array') {
76
+ // Special handling for common array types
77
+ if (propName === 'choices' && Array.isArray(value)) {
78
+ html += renderChoicesEditor(value, item.correctAnswer);
79
+ } else if (propName === 'pairs' && Array.isArray(value)) {
80
+ html += renderPairsEditor(value);
81
+ } else if (propName === 'items' && Array.isArray(value)) {
82
+ html += renderItemsEditor(value);
83
+ } else if ((propName === 'zones' || propName === 'dropZones') && Array.isArray(value)) {
84
+ html += renderZonesEditor(value);
85
+ } else if (propName === 'scale' && Array.isArray(value)) {
86
+ html += renderScaleEditor(value);
87
+ } else if (propName === 'questions' && Array.isArray(value)) {
88
+ html += renderSubQuestionsEditor(value);
89
+ } else {
90
+ // Generic array display
91
+ html += `<textarea name="${propName}" rows="3" readonly>${escapeHtml(JSON.stringify(value, null, 2))}</textarea>`;
92
+ }
93
+ } else if (propName === 'template') {
94
+ // Fill-in-the-blank template with syntax helper
95
+ html += `<textarea name="${propName}" rows="3">${escapeHtml(value || '')}</textarea>`;
96
+ html += '<div class="template-syntax-help">Use <code>{{answer}}</code> for single blank or <code>{{1:answer}}</code> for multiple blanks</div>';
97
+ } else if (propName === 'prompt' || propDef.multiline) {
98
+ html += `<textarea name="${propName}" rows="2">${escapeHtml(value || '')}</textarea>`;
99
+ } else if (propDef.type === 'number') {
100
+ html += `<input type="number" name="${propName}" value="${isPresent ? value : ''}" />`;
101
+ } else {
102
+ html += `<input type="text" name="${propName}" value="${escapeHtml(value || '')}" />`;
103
+ }
104
+
105
+ html += '</div>';
106
+ }
107
+
108
+ html += `<div class="edit-actions">
109
+ <button class="edit-save-btn">💾 Save</button>
110
+ <button class="edit-cancel-btn">Cancel</button>
111
+ </div>`;
112
+
113
+ html += '</div>';
114
+ return html;
115
+ }
116
+
117
+ /**
118
+ * Format a camelCase property name as a human-readable label
119
+ */
120
+ function formatLabel(propName) {
121
+ return propName
122
+ .replace(/([A-Z])/g, ' $1')
123
+ .replace(/^./, s => s.toUpperCase())
124
+ .trim();
125
+ }
126
+
127
+ /**
128
+ * Render choices editor for multiple-choice questions
129
+ */
130
+ function renderChoicesEditor(choices, correctAnswer) {
131
+ let html = '<div class="edit-choices">';
132
+ for (let i = 0; i < choices.length; i++) {
133
+ const c = choices[i];
134
+ const isCorrect = c.value === correctAnswer || c.correct === true;
135
+ html += `<div class="edit-choice" data-index="${i}">
136
+ <input type="radio" name="correctChoice" value="${c.value}" ${isCorrect ? 'checked' : ''} />
137
+ <span class="choice-value">${c.value}:</span>
138
+ <input type="text" name="choice-${i}" value="${escapeHtml(c.text)}" class="choice-text" />
139
+ </div>`;
140
+ }
141
+ html += '</div>';
142
+ return html;
143
+ }
144
+
145
+ /**
146
+ * Render pairs editor for matching questions
147
+ */
148
+ function renderPairsEditor(pairs) {
149
+ let html = '<div class="edit-pairs">';
150
+ for (let i = 0; i < pairs.length; i++) {
151
+ const p = pairs[i];
152
+ html += `<div class="edit-pair" data-index="${i}">
153
+ <input type="text" name="pair-text-${i}" value="${escapeHtml(p.text)}" placeholder="Item" />
154
+ <span class="pair-arrow">→</span>
155
+ <input type="text" name="pair-match-${i}" value="${escapeHtml(p.match)}" placeholder="Match" />
156
+ </div>`;
157
+ }
158
+ html += '</div>';
159
+ return html;
160
+ }
161
+
162
+ /**
163
+ * Render items editor for drag-drop/sequencing
164
+ */
165
+ function renderItemsEditor(items) {
166
+ let html = '<div class="edit-items">';
167
+ for (let i = 0; i < items.length; i++) {
168
+ const item = items[i];
169
+ const text = typeof item === 'string' ? item : (item.text || item.id || JSON.stringify(item));
170
+ html += `<div class="edit-item" data-index="${i}">
171
+ <span class="item-index">${i + 1}.</span>
172
+ <input type="text" name="item-${i}" value="${escapeHtml(text)}" />
173
+ </div>`;
174
+ }
175
+ html += '</div>';
176
+ return html;
177
+ }
178
+
179
+ /**
180
+ * Render zones editor for drag-drop questions
181
+ */
182
+ function renderZonesEditor(zones) {
183
+ let html = '<div class="edit-zones">';
184
+ for (let i = 0; i < zones.length; i++) {
185
+ const zone = zones[i];
186
+ const id = typeof zone === 'string' ? zone : (zone.id || '');
187
+ const label = typeof zone === 'string' ? zone : (zone.label || zone.text || zone.id || '');
188
+ html += `<div class="edit-zone" data-index="${i}">
189
+ <input type="text" name="zone-id-${i}" value="${escapeHtml(id)}" placeholder="Zone ID" class="zone-id" />
190
+ <input type="text" name="zone-label-${i}" value="${escapeHtml(label)}" placeholder="Zone Label" class="zone-label" />
191
+ </div>`;
192
+ }
193
+ html += '</div>';
194
+ return html;
195
+ }
196
+
197
+ /**
198
+ * Render scale editor for likert questions
199
+ */
200
+ function renderScaleEditor(scale) {
201
+ let html = '<div class="edit-scale">';
202
+ for (let i = 0; i < scale.length; i++) {
203
+ const point = scale[i];
204
+ html += `<div class="edit-scale-point" data-index="${i}">
205
+ <input type="text" name="scale-value-${i}" value="${escapeHtml(point.value || String(i + 1))}" class="scale-value" />
206
+ <input type="text" name="scale-label-${i}" value="${escapeHtml(point.label || '')}" class="scale-label" placeholder="Label" />
207
+ </div>`;
208
+ }
209
+ html += '</div>';
210
+ return html;
211
+ }
212
+
213
+ /**
214
+ * Render sub-questions editor for likert (multi-statement)
215
+ */
216
+ function renderSubQuestionsEditor(questions) {
217
+ let html = '<div class="edit-subquestions">';
218
+ for (let i = 0; i < questions.length; i++) {
219
+ const q = questions[i];
220
+ html += `<div class="edit-subquestion" data-index="${i}">
221
+ <span class="subq-index">${i + 1}.</span>
222
+ <input type="text" name="subq-${i}" value="${escapeHtml(q.text || q)}" />
223
+ </div>`;
224
+ }
225
+ html += '</div>';
226
+ return html;
227
+ }
228
+
229
+ /**
230
+ * Legacy fallback for when no schema is available
231
+ */
232
+ function renderLegacyEditForm(item) {
233
+ let html = '<div class="edit-form">';
234
+
235
+ // Label field
236
+ if (item.label !== undefined) {
237
+ html += `<div class="edit-field">
238
+ <label>Label</label>
239
+ <input type="text" name="label" value="${escapeHtml(item.label)}" />
240
+ </div>`;
241
+ }
242
+
243
+ // Prompt field
244
+ if (item.prompt !== undefined) {
245
+ html += `<div class="edit-field">
246
+ <label>Prompt</label>
247
+ <textarea name="prompt" rows="2">${escapeHtml(item.prompt)}</textarea>
248
+ </div>`;
249
+ }
250
+
251
+ // Correct answer field (for T/F, text input, numeric - not MC)
252
+ if (item.correctAnswer !== undefined && item.type !== 'multiple-choice') {
253
+ if (item.type === 'true-false') {
254
+ const isTrue = item.correctAnswer === true || item.correctAnswer === 'true';
255
+ html += `<div class="edit-field">
256
+ <label>Correct Answer</label>
257
+ <select name="correctAnswer">
258
+ <option value="true" ${isTrue ? 'selected' : ''}>True</option>
259
+ <option value="false" ${!isTrue ? 'selected' : ''}>False</option>
260
+ </select>
261
+ </div>`;
262
+ } else {
263
+ html += `<div class="edit-field">
264
+ <label>Correct Answer</label>
265
+ <input type="text" name="correctAnswer" value="${escapeHtml(item.correctAnswer)}" />
266
+ </div>`;
267
+ }
268
+ }
269
+
270
+ // Choices field (for multiple-choice)
271
+ if (item.choices && item.choices.length > 0) {
272
+ html += '<div class="edit-field"><label>Choices</label>';
273
+ html += renderChoicesEditor(item.choices, item.correctAnswer);
274
+ html += '</div>';
275
+ }
276
+
277
+ // Pairs field (for matching)
278
+ if (item.pairs && item.pairs.length > 0) {
279
+ html += '<div class="edit-field"><label>Pairs</label>';
280
+ html += renderPairsEditor(item.pairs);
281
+ html += '</div>';
282
+ }
283
+
284
+ html += `<div class="edit-actions">
285
+ <button class="edit-save-btn">💾 Save</button>
286
+ <button class="edit-cancel-btn">Cancel</button>
287
+ </div>`;
288
+
289
+ html += '</div>';
290
+ return html;
291
+ }
292
+
293
+ /**
294
+ * Save edits by posting to the server
295
+ * @param {string} endpoint - API endpoint (e.g., '/__edit-interaction')
296
+ * @param {Element} form - The form DOM element
297
+ * @param {string} slideId - Slide identifier
298
+ * @param {string} itemId - Interaction or question ID
299
+ * @returns {Promise<boolean>} Success status
300
+ */
301
+ export async function saveItemEdits(endpoint, form, slideId, itemId) {
302
+ const edits = {};
303
+
304
+ // 1. Label
305
+ const labelInput = form.querySelector('[name="label"]');
306
+ if (labelInput) edits.label = labelInput.value;
307
+
308
+ // 2. Prompt
309
+ const promptInput = form.querySelector('[name="prompt"]');
310
+ if (promptInput) edits.prompt = promptInput.value;
311
+
312
+ // 3. Choices (Array)
313
+ const choiceInputs = form.querySelectorAll('[name^="choice-"]');
314
+ if (choiceInputs.length > 0) {
315
+ // Collect choices by finding container elements to ensure we get value+text pairs
316
+ const choiceContainers = form.querySelectorAll('.edit-choice');
317
+ const choices = [];
318
+
319
+ choiceContainers.forEach(container => {
320
+ const textInput = container.querySelector('.choice-text');
321
+ // The radio value typically holds the choice ID/Value
322
+ const radio = container.querySelector('input[type="radio"]');
323
+
324
+ if (textInput && radio) {
325
+ choices.push({
326
+ value: radio.value,
327
+ text: textInput.value
328
+ });
329
+ }
330
+ });
331
+
332
+ if (choices.length > 0) edits.choices = choices;
333
+ }
334
+
335
+ // 4. Pairs (Matching)
336
+ const pairContainers = form.querySelectorAll('.edit-pair');
337
+ if (pairContainers.length > 0) {
338
+ const pairs = [];
339
+ pairContainers.forEach(container => {
340
+ const textInput = container.querySelector('input[placeholder="Item"]');
341
+ const matchInput = container.querySelector('input[placeholder="Match"]');
342
+ if (textInput && matchInput) {
343
+ pairs.push({
344
+ text: textInput.value,
345
+ match: matchInput.value
346
+ });
347
+ }
348
+ });
349
+ if (pairs.length > 0) edits.pairs = pairs;
350
+ }
351
+
352
+ // 5. Correct Answer
353
+ // Handle Boolean (True/False)
354
+ const correctSelect = form.querySelector('select[name="correctAnswer"]');
355
+ if (correctSelect) {
356
+ edits.correctAnswer = correctSelect.value === 'true';
357
+ } else {
358
+ // Handle Text Input
359
+ const correctInput = form.querySelector('input[name="correctAnswer"]');
360
+ if (correctInput) {
361
+ edits.correctAnswer = correctInput.value;
362
+ } else {
363
+ // Handle Choice Radio
364
+ const correctRadio = form.querySelector('input[name="correctChoice"]:checked');
365
+ if (correctRadio) {
366
+ edits.correctAnswer = correctRadio.value;
367
+ }
368
+ }
369
+ }
370
+
371
+ // 6. Generic Loop for other simple fields
372
+ const otherInputs = form.querySelectorAll('input:not([name^="choice-"]):not([name^="pair-"]):not([name^="item-"]):not([name="correctAnswer"]):not([name="correctChoice"]), select:not([name="correctAnswer"])');
373
+ otherInputs.forEach(input => {
374
+ if (input.type === 'radio' || input.type === 'checkbox' || input.type === 'submit' || input.type === 'button') return;
375
+ if (input.name && !edits[input.name]) {
376
+ // Basic type conversion
377
+ edits[input.name] = input.type === 'number' ? parseFloat(input.value) : input.value;
378
+ }
379
+ });
380
+
381
+ try {
382
+ const res = await fetch(endpoint, {
383
+ method: 'POST',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ slideId, interactionId: itemId, edits })
386
+ });
387
+
388
+ if (!res.ok) {
389
+ const data = await res.json();
390
+ console.error('Edit failed:', data.error);
391
+ alert('Failed to save: ' + (data.error || 'Unknown error'));
392
+ return false;
393
+ }
394
+ return true;
395
+ } catch (err) {
396
+ console.error('Edit error:', err);
397
+ alert('Error saving edits: ' + err.message);
398
+ return false;
399
+ }
400
+ }