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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stub-player/header-bar.js - Header bar component
|
|
3
|
+
*
|
|
4
|
+
* Generates the header bar HTML with all control buttons.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const HEADER_ICON_PATHS = {
|
|
8
|
+
monitor: '<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
|
|
9
|
+
edit: '<path d="M13 21h8"/><path d="m15 5 4 4"/><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>',
|
|
10
|
+
review: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
|
11
|
+
debug: '<path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/>',
|
|
12
|
+
more: '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
|
|
13
|
+
guide: '<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/><circle cx="12" cy="12" r="10"/>',
|
|
14
|
+
config: '<line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="2" x2="6" y1="14" y2="14"/><line x1="10" x2="14" y1="8" y2="8"/><line x1="18" x2="22" y1="16" y2="16"/>',
|
|
15
|
+
interactions: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
|
16
|
+
catalog: '<path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/>',
|
|
17
|
+
slideId: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
|
|
18
|
+
skipGating: '<circle cx="15" cy="12" r="3"/><rect width="20" height="14" x="2" y="5" rx="7"/>',
|
|
19
|
+
reset: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
|
|
20
|
+
minimize: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
|
|
21
|
+
maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function renderHeaderIcon(name, className = 'header-icon') {
|
|
25
|
+
const path = HEADER_ICON_PATHS[name];
|
|
26
|
+
if (!path) return '';
|
|
27
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true">${path}</svg>`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderCourseCodeLogo(className = 'header-logo') {
|
|
31
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 18 100 64" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true"><polyline points="25,22 5,50 25,78"/><polyline points="75,22 95,50 75,78"/><path d="M50,28 C40,28 33,36 33,45 C33,52 38,56 42,60 L42,65 L58,65 L58,60 C62,56 67,52 67,45 C67,36 60,28 50,28" stroke-width="4"/><line x1="44" y1="70" x2="56" y2="70" stroke-width="4"/><line x1="46" y1="75" x2="54" y2="75" stroke-width="4"/></svg>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SHOW_SLIDE_ID_STORAGE_KEY = 'coursecode-showSlideId';
|
|
35
|
+
const SKIP_GATING_STORAGE_KEY = 'coursecode-skipGating';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate header bar HTML
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
* @param {boolean} options.isLive - Whether this is live mode
|
|
41
|
+
* @param {boolean} options.hasContent - Whether content viewer should be shown
|
|
42
|
+
*/
|
|
43
|
+
export function generateHeaderBar({ isLive, hasContent }) {
|
|
44
|
+
return `
|
|
45
|
+
<div id="stub-player-header">
|
|
46
|
+
<div class="header-content">
|
|
47
|
+
<span class="label">
|
|
48
|
+
${renderCourseCodeLogo('header-logo')}
|
|
49
|
+
CourseCode
|
|
50
|
+
</span>
|
|
51
|
+
${isLive ? '<span id="stub-player-slide-id" class="slide-id-badge" title="Current slide ID"></span>' : ''}
|
|
52
|
+
<div class="spacer"></div>
|
|
53
|
+
${isLive ? `<button id="stub-player-edit-mode-btn" class="visible" title="Edit text in course">${renderHeaderIcon('edit')} <span class="btn-label">Edit</span></button>` : ''}
|
|
54
|
+
${hasContent ? `<button id="stub-player-content-btn" title="Review content">${renderHeaderIcon('review')} <span class="btn-label">Review</span></button>` : ''}
|
|
55
|
+
${isLive ? `<button id="stub-player-debug-btn" title="Debug panel">${renderHeaderIcon('debug')} <span class="btn-label">Debug</span><span id="stub-player-header-error-badge-inline"></span></button>` : ''}
|
|
56
|
+
<div class="more-menu-wrap">
|
|
57
|
+
<button id="stub-player-more-btn" title="More tools">${renderHeaderIcon('more')} <span class="btn-label">More</span></button>
|
|
58
|
+
<div id="stub-player-more-menu" class="more-menu">
|
|
59
|
+
${isLive ? `<button id="stub-player-status-btn" class="menu-item" title="Workflow guide">${renderHeaderIcon('guide', 'header-icon menu-icon')} Guide</button>` : ''}
|
|
60
|
+
${isLive ? `<button id="stub-player-config-btn" class="menu-item" title="Course config">${renderHeaderIcon('config', 'header-icon menu-icon')} Config</button>` : ''}
|
|
61
|
+
${isLive ? `<button id="stub-player-interactions-btn" class="menu-item" title="Interactions">${renderHeaderIcon('interactions', 'header-icon menu-icon')} Interactions</button>` : ''}
|
|
62
|
+
${isLive ? `<button id="stub-player-catalog-btn" class="menu-item" title="Component catalog">${renderHeaderIcon('catalog', 'header-icon menu-icon')} UI Catalog</button>` : ''}
|
|
63
|
+
${isLive ? `<div class="menu-divider"></div>
|
|
64
|
+
<div class="menu-toggle-row" title="Show or hide current slide ID in header">
|
|
65
|
+
<span class="menu-toggle-label-wrap">${renderHeaderIcon('slideId', 'header-icon menu-icon')} <span id="stub-player-show-slide-id-label">Show Slide ID: On</span></span>
|
|
66
|
+
<input type="checkbox" id="stub-player-show-slide-id" class="menu-toggle-input" checked aria-label="Show Slide ID">
|
|
67
|
+
<button id="stub-player-show-slide-id-toggle" class="config-toggle on" type="button" role="switch" aria-checked="true" aria-label="Show Slide ID"></button>
|
|
68
|
+
</div>` : ''}
|
|
69
|
+
<div class="menu-toggle-row" title="Skip navigation gating">
|
|
70
|
+
<span class="menu-toggle-label-wrap">${renderHeaderIcon('skipGating', 'header-icon menu-icon')} <span id="stub-player-skip-gating-label">Skip Gating: On</span></span>
|
|
71
|
+
<input type="checkbox" id="stub-player-skip-gating" class="menu-toggle-input" checked aria-label="Skip Gating">
|
|
72
|
+
<button id="stub-player-skip-gating-toggle" class="config-toggle on" type="button" role="switch" aria-checked="true" aria-label="Skip Gating"></button>
|
|
73
|
+
</div>
|
|
74
|
+
<button id="stub-player-reset-btn" class="menu-item danger-item" title="Reset progress">${renderHeaderIcon('reset', 'header-icon menu-icon')} Reset Progress</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<button class="toggle-btn" id="stub-player-header-toggle" title="Minimize preview" aria-label="Minimize preview">${renderHeaderIcon('minimize')}</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div id="stub-player-confirm-dialog" class="confirm-dialog">
|
|
81
|
+
<div class="confirm-content">
|
|
82
|
+
<h3 id="stub-player-confirm-title">Confirm</h3>
|
|
83
|
+
<p id="stub-player-confirm-message"></p>
|
|
84
|
+
<div class="confirm-actions">
|
|
85
|
+
<button id="stub-player-confirm-cancel">Cancel</button>
|
|
86
|
+
<button id="stub-player-confirm-ok" class="btn-primary">OK</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize Header Bar Handlers
|
|
95
|
+
* @param {Object} callbacks
|
|
96
|
+
* @param {Function} callbacks.onToggle - (isCollapsed) => void
|
|
97
|
+
* @param {Function} callbacks.onDebug - () => void
|
|
98
|
+
* @param {Function} callbacks.onConfig - () => void
|
|
99
|
+
* @param {Function} callbacks.onContent - () => void
|
|
100
|
+
* @param {Function} callbacks.onInteract - () => void
|
|
101
|
+
* @param {Function} callbacks.onEdit - () => void
|
|
102
|
+
* @param {Function} callbacks.onReset - () => void
|
|
103
|
+
* @param {Function} callbacks.onSkipGating - (enabled) => void
|
|
104
|
+
* @param {Function} callbacks.onStatus - () => void
|
|
105
|
+
*/
|
|
106
|
+
export function createHeaderBarHandlers(callbacks) {
|
|
107
|
+
const { onToggle, onDebug, onConfig, onContent, onInteract, onCatalog, onEdit, onReset, onSkipGating, onStatus } = callbacks;
|
|
108
|
+
const closeMoreMenu = () => {
|
|
109
|
+
document.getElementById('stub-player-more-menu')?.classList.remove('visible');
|
|
110
|
+
};
|
|
111
|
+
const updateSlideIdVisibility = (isVisible) => {
|
|
112
|
+
const header = document.getElementById('stub-player-header');
|
|
113
|
+
if (!header) return;
|
|
114
|
+
header.classList.toggle('hide-slide-id', !isVisible);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Header Toggle
|
|
118
|
+
const toggleBtn = document.getElementById('stub-player-header-toggle');
|
|
119
|
+
if (toggleBtn) {
|
|
120
|
+
toggleBtn.addEventListener('click', () => {
|
|
121
|
+
const header = document.getElementById('stub-player-header');
|
|
122
|
+
const iframe = document.getElementById('stub-player-course-frame');
|
|
123
|
+
const moreMenu = document.getElementById('stub-player-more-menu');
|
|
124
|
+
|
|
125
|
+
header.classList.toggle('collapsed');
|
|
126
|
+
const isCollapsed = header.classList.contains('collapsed');
|
|
127
|
+
if (iframe) iframe.classList.toggle('header-collapsed', isCollapsed);
|
|
128
|
+
if (moreMenu) moreMenu.classList.remove('visible');
|
|
129
|
+
toggleBtn.innerHTML = isCollapsed
|
|
130
|
+
? `${renderHeaderIcon('maximize')}`
|
|
131
|
+
: `${renderHeaderIcon('minimize')}`;
|
|
132
|
+
toggleBtn.title = isCollapsed ? 'Restore preview' : 'Minimize preview';
|
|
133
|
+
toggleBtn.setAttribute('aria-label', isCollapsed ? 'Restore preview' : 'Minimize preview');
|
|
134
|
+
|
|
135
|
+
if (onToggle) onToggle(isCollapsed);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// More Menu
|
|
140
|
+
const moreBtn = document.getElementById('stub-player-more-btn');
|
|
141
|
+
const moreMenu = document.getElementById('stub-player-more-menu');
|
|
142
|
+
if (moreBtn && moreMenu) {
|
|
143
|
+
moreBtn.addEventListener('click', (e) => {
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
moreMenu.classList.toggle('visible');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
document.addEventListener('click', (e) => {
|
|
149
|
+
if (!moreMenu.contains(e.target) && !moreBtn.contains(e.target)) {
|
|
150
|
+
moreMenu.classList.remove('visible');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Show Slide ID
|
|
156
|
+
const showSlideIdCheckbox = document.getElementById('stub-player-show-slide-id');
|
|
157
|
+
const showSlideIdToggle = document.getElementById('stub-player-show-slide-id-toggle');
|
|
158
|
+
const showSlideIdLabel = document.getElementById('stub-player-show-slide-id-label');
|
|
159
|
+
if (showSlideIdCheckbox) {
|
|
160
|
+
const persisted = (() => {
|
|
161
|
+
try {
|
|
162
|
+
const stored = localStorage.getItem(SHOW_SLIDE_ID_STORAGE_KEY);
|
|
163
|
+
return stored !== 'false';
|
|
164
|
+
} catch {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
showSlideIdCheckbox.checked = persisted;
|
|
169
|
+
|
|
170
|
+
const syncSlideIdUi = () => {
|
|
171
|
+
if (showSlideIdToggle) {
|
|
172
|
+
showSlideIdToggle.classList.toggle('on', showSlideIdCheckbox.checked);
|
|
173
|
+
showSlideIdToggle.setAttribute('aria-checked', showSlideIdCheckbox.checked ? 'true' : 'false');
|
|
174
|
+
}
|
|
175
|
+
if (showSlideIdLabel) {
|
|
176
|
+
showSlideIdLabel.textContent = `Show Slide ID: ${showSlideIdCheckbox.checked ? 'On' : 'Off'}`;
|
|
177
|
+
}
|
|
178
|
+
updateSlideIdVisibility(showSlideIdCheckbox.checked);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
syncSlideIdUi();
|
|
182
|
+
|
|
183
|
+
if (showSlideIdToggle) {
|
|
184
|
+
const toggleShowSlideId = () => {
|
|
185
|
+
showSlideIdCheckbox.checked = !showSlideIdCheckbox.checked;
|
|
186
|
+
showSlideIdCheckbox.dispatchEvent(new Event('change', { bubbles: true }));
|
|
187
|
+
};
|
|
188
|
+
showSlideIdToggle.addEventListener('click', toggleShowSlideId);
|
|
189
|
+
showSlideIdToggle.addEventListener('keydown', (e) => {
|
|
190
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
toggleShowSlideId();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
showSlideIdCheckbox.addEventListener('change', () => {
|
|
198
|
+
try {
|
|
199
|
+
localStorage.setItem(SHOW_SLIDE_ID_STORAGE_KEY, showSlideIdCheckbox.checked ? 'true' : 'false');
|
|
200
|
+
} catch {
|
|
201
|
+
// ignore storage failures
|
|
202
|
+
}
|
|
203
|
+
syncSlideIdUi();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Skip Gating (persisted in localStorage)
|
|
208
|
+
const skipGatingCheckbox = document.getElementById('stub-player-skip-gating');
|
|
209
|
+
const skipGatingToggle = document.getElementById('stub-player-skip-gating-toggle');
|
|
210
|
+
const skipGatingLabel = document.getElementById('stub-player-skip-gating-label');
|
|
211
|
+
if (skipGatingCheckbox) {
|
|
212
|
+
// Restore persisted state (defaults to true / skip on)
|
|
213
|
+
const persistedSkip = (() => {
|
|
214
|
+
try {
|
|
215
|
+
const stored = localStorage.getItem(SKIP_GATING_STORAGE_KEY);
|
|
216
|
+
return stored !== 'false';
|
|
217
|
+
} catch {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
221
|
+
skipGatingCheckbox.checked = persistedSkip;
|
|
222
|
+
|
|
223
|
+
const syncSkipToggleUi = () => {
|
|
224
|
+
if (!skipGatingToggle) return;
|
|
225
|
+
skipGatingToggle.classList.toggle('on', skipGatingCheckbox.checked);
|
|
226
|
+
skipGatingToggle.setAttribute('aria-checked', skipGatingCheckbox.checked ? 'true' : 'false');
|
|
227
|
+
if (skipGatingLabel) {
|
|
228
|
+
skipGatingLabel.textContent = `Skip Gating: ${skipGatingCheckbox.checked ? 'On' : 'Off'}`;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
syncSkipToggleUi();
|
|
233
|
+
|
|
234
|
+
if (skipGatingToggle) {
|
|
235
|
+
const toggleSkip = () => {
|
|
236
|
+
skipGatingCheckbox.checked = !skipGatingCheckbox.checked;
|
|
237
|
+
skipGatingCheckbox.dispatchEvent(new Event('change', { bubbles: true }));
|
|
238
|
+
};
|
|
239
|
+
skipGatingToggle.addEventListener('click', toggleSkip);
|
|
240
|
+
skipGatingToggle.addEventListener('keydown', (e) => {
|
|
241
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
toggleSkip();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
skipGatingCheckbox.addEventListener('change', (e) => {
|
|
249
|
+
if (!e.target.checked) {
|
|
250
|
+
// Unchecking = enabling gating, show confirmation
|
|
251
|
+
showConfirmDialog({
|
|
252
|
+
title: 'Enable Gating?',
|
|
253
|
+
message: 'This will reset course progress and enforce navigation locks.',
|
|
254
|
+
confirmLabel: 'Confirm',
|
|
255
|
+
confirmClass: 'btn-primary',
|
|
256
|
+
onConfirm: () => {
|
|
257
|
+
try { localStorage.setItem(SKIP_GATING_STORAGE_KEY, 'false'); } catch {}
|
|
258
|
+
if (onSkipGating) onSkipGating(false);
|
|
259
|
+
syncSkipToggleUi();
|
|
260
|
+
},
|
|
261
|
+
onCancel: () => {
|
|
262
|
+
e.target.checked = true; // Revert
|
|
263
|
+
syncSkipToggleUi();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
// Checking = skip gating
|
|
268
|
+
try { localStorage.setItem(SKIP_GATING_STORAGE_KEY, 'true'); } catch {}
|
|
269
|
+
if (onSkipGating) onSkipGating(true);
|
|
270
|
+
syncSkipToggleUi();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Reset Button
|
|
276
|
+
const resetBtn = document.getElementById('stub-player-reset-btn');
|
|
277
|
+
if (resetBtn) {
|
|
278
|
+
resetBtn.addEventListener('click', () => {
|
|
279
|
+
closeMoreMenu();
|
|
280
|
+
showConfirmDialog({
|
|
281
|
+
title: 'Reset Progress?',
|
|
282
|
+
message: 'This will clear all course progress. This cannot be undone.',
|
|
283
|
+
confirmLabel: 'Reset',
|
|
284
|
+
confirmClass: 'btn-danger',
|
|
285
|
+
onConfirm: () => {
|
|
286
|
+
if (onReset) onReset();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Panel Buttons
|
|
293
|
+
if (onDebug) document.getElementById('stub-player-debug-btn')?.addEventListener('click', onDebug);
|
|
294
|
+
if (onConfig) document.getElementById('stub-player-config-btn')?.addEventListener('click', () => { closeMoreMenu(); onConfig(); });
|
|
295
|
+
if (onContent) document.getElementById('stub-player-content-btn')?.addEventListener('click', onContent);
|
|
296
|
+
if (onInteract) document.getElementById('stub-player-interactions-btn')?.addEventListener('click', () => { closeMoreMenu(); onInteract(); });
|
|
297
|
+
if (onCatalog) document.getElementById('stub-player-catalog-btn')?.addEventListener('click', () => { closeMoreMenu(); onCatalog(); });
|
|
298
|
+
if (onEdit) document.getElementById('stub-player-edit-mode-btn')?.addEventListener('click', onEdit);
|
|
299
|
+
if (onStatus) document.getElementById('stub-player-status-btn')?.addEventListener('click', () => { closeMoreMenu(); onStatus(); });
|
|
300
|
+
|
|
301
|
+
// Setup Confirm Dialog Handlers
|
|
302
|
+
setupConfirmDialog();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Dialog Logic
|
|
306
|
+
let pendingConfirmCallback = null;
|
|
307
|
+
let pendingCancelCallback = null;
|
|
308
|
+
|
|
309
|
+
function setupConfirmDialog() {
|
|
310
|
+
const okBtn = document.getElementById('stub-player-confirm-ok');
|
|
311
|
+
const cancelBtn = document.getElementById('stub-player-confirm-cancel');
|
|
312
|
+
const dialog = document.getElementById('stub-player-confirm-dialog');
|
|
313
|
+
|
|
314
|
+
if (okBtn) {
|
|
315
|
+
okBtn.addEventListener('click', () => {
|
|
316
|
+
dialog.classList.remove('visible');
|
|
317
|
+
if (pendingConfirmCallback) {
|
|
318
|
+
pendingConfirmCallback();
|
|
319
|
+
pendingConfirmCallback = null;
|
|
320
|
+
pendingCancelCallback = null;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (cancelBtn) {
|
|
326
|
+
cancelBtn.addEventListener('click', () => {
|
|
327
|
+
dialog.classList.remove('visible');
|
|
328
|
+
if (pendingCancelCallback) {
|
|
329
|
+
pendingCancelCallback();
|
|
330
|
+
}
|
|
331
|
+
pendingConfirmCallback = null;
|
|
332
|
+
pendingCancelCallback = null;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function showConfirmDialog({ title, message, confirmLabel = 'Confirm', confirmClass = 'btn-primary', onConfirm, onCancel }) {
|
|
338
|
+
document.getElementById('stub-player-confirm-title').textContent = title;
|
|
339
|
+
document.getElementById('stub-player-confirm-message').textContent = message;
|
|
340
|
+
const confirmBtn = document.getElementById('stub-player-confirm-ok');
|
|
341
|
+
confirmBtn.textContent = confirmLabel;
|
|
342
|
+
confirmBtn.className = confirmClass;
|
|
343
|
+
pendingConfirmCallback = onConfirm;
|
|
344
|
+
pendingCancelCallback = onCancel;
|
|
345
|
+
document.getElementById('stub-player-confirm-dialog').classList.add('visible');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function updateSlideId(id) {
|
|
349
|
+
const badge = document.getElementById('stub-player-slide-id');
|
|
350
|
+
if (badge) {
|
|
351
|
+
badge.textContent = id;
|
|
352
|
+
badge.title = 'Current slide: ' + id;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stub-player/interaction-editor.js - Popup modal for editing interactions
|
|
3
|
+
*
|
|
4
|
+
* Provides a reusable popup editor that opens when clicking on interaction
|
|
5
|
+
* components in the preview iframe. Reuses renderEditForm from edit-utils.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { renderEditForm, saveItemEdits } from './edit-utils.js';
|
|
9
|
+
|
|
10
|
+
let currentInteraction = null;
|
|
11
|
+
let onSaveCallback = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate the editor modal HTML
|
|
15
|
+
*/
|
|
16
|
+
export function generateInteractionEditor() {
|
|
17
|
+
return `
|
|
18
|
+
<div id="stub-player-interaction-editor" class="interaction-editor-overlay">
|
|
19
|
+
<div class="interaction-editor-modal">
|
|
20
|
+
<div class="interaction-editor-header">
|
|
21
|
+
<div class="interaction-editor-title">
|
|
22
|
+
<span class="interaction-type-badge"></span>
|
|
23
|
+
<span class="interaction-id-text"></span>
|
|
24
|
+
</div>
|
|
25
|
+
<button class="interaction-editor-close" title="Close">×</button>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="interaction-editor-body">
|
|
28
|
+
<!-- Form content will be injected here -->
|
|
29
|
+
</div>
|
|
30
|
+
<div class="interaction-editor-footer">
|
|
31
|
+
<button class="interaction-editor-cancel">Cancel</button>
|
|
32
|
+
<button class="interaction-editor-save btn-primary">💾 Save</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the interaction editor handlers
|
|
41
|
+
* @param {Object} options
|
|
42
|
+
* @param {Function} options.onSave - Callback after successful save (refresh content)
|
|
43
|
+
*/
|
|
44
|
+
export function initInteractionEditor(options = {}) {
|
|
45
|
+
onSaveCallback = options.onSave || null;
|
|
46
|
+
|
|
47
|
+
const overlay = document.getElementById('stub-player-interaction-editor');
|
|
48
|
+
if (!overlay) return;
|
|
49
|
+
|
|
50
|
+
const closeBtn = overlay.querySelector('.interaction-editor-close');
|
|
51
|
+
const cancelBtn = overlay.querySelector('.interaction-editor-cancel');
|
|
52
|
+
const saveBtn = overlay.querySelector('.interaction-editor-save');
|
|
53
|
+
|
|
54
|
+
// Close handlers
|
|
55
|
+
closeBtn?.addEventListener('click', closeEditor);
|
|
56
|
+
cancelBtn?.addEventListener('click', closeEditor);
|
|
57
|
+
|
|
58
|
+
// Click outside to close
|
|
59
|
+
overlay.addEventListener('click', (e) => {
|
|
60
|
+
if (e.target === overlay) {
|
|
61
|
+
closeEditor();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Escape key to close
|
|
66
|
+
document.addEventListener('keydown', (e) => {
|
|
67
|
+
if (e.key === 'Escape' && overlay.classList.contains('visible')) {
|
|
68
|
+
closeEditor();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Save handler
|
|
73
|
+
saveBtn?.addEventListener('click', handleSave);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Open the editor for a specific interaction
|
|
80
|
+
* @param {Object} interaction - The interaction data object
|
|
81
|
+
* @param {string} slideId - The slide ID containing this interaction
|
|
82
|
+
*/
|
|
83
|
+
export async function openEditor(interaction, slideId) {
|
|
84
|
+
if (!interaction || !interaction.id) {
|
|
85
|
+
console.warn('Cannot open editor: invalid interaction data');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
currentInteraction = { ...interaction, slideId };
|
|
90
|
+
|
|
91
|
+
const overlay = document.getElementById('stub-player-interaction-editor');
|
|
92
|
+
const body = overlay?.querySelector('.interaction-editor-body');
|
|
93
|
+
const typeBadge = overlay?.querySelector('.interaction-type-badge');
|
|
94
|
+
const idText = overlay?.querySelector('.interaction-id-text');
|
|
95
|
+
|
|
96
|
+
if (!overlay || !body) return;
|
|
97
|
+
|
|
98
|
+
// Set header info
|
|
99
|
+
if (typeBadge) typeBadge.textContent = interaction.type || 'unknown';
|
|
100
|
+
if (idText) idText.textContent = interaction.id;
|
|
101
|
+
|
|
102
|
+
// Use schema from interaction (provided by manifest)
|
|
103
|
+
const schema = interaction.schema || null;
|
|
104
|
+
|
|
105
|
+
body.innerHTML = renderEditForm(interaction, schema);
|
|
106
|
+
|
|
107
|
+
// Show the modal
|
|
108
|
+
overlay.classList.add('visible');
|
|
109
|
+
|
|
110
|
+
// Focus first input
|
|
111
|
+
const firstInput = body.querySelector('input, textarea, select');
|
|
112
|
+
if (firstInput) firstInput.focus();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Open editor by fetching interaction data from server
|
|
117
|
+
* @param {string} interactionId - The interaction ID
|
|
118
|
+
* @param {string} slideId - The slide ID
|
|
119
|
+
*/
|
|
120
|
+
export async function openEditorById(interactionId, slideId) {
|
|
121
|
+
try {
|
|
122
|
+
// Fetch interactions from manifest
|
|
123
|
+
const response = await fetch('/_content-manifest.json');
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
console.error('Failed to fetch manifest');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const manifest = await response.json();
|
|
130
|
+
|
|
131
|
+
// Collect all interactions from slides
|
|
132
|
+
let interaction = null;
|
|
133
|
+
for (const [sId, slideData] of Object.entries(manifest.slides || {})) {
|
|
134
|
+
const found = (slideData.interactions || []).find(i => i.id === interactionId);
|
|
135
|
+
if (found) {
|
|
136
|
+
interaction = { ...found, slideId: sId };
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If not found in standalone interactions, check assessments
|
|
142
|
+
if (!interaction) {
|
|
143
|
+
for (const assessment of (manifest.assessments || [])) {
|
|
144
|
+
const question = (assessment.questions || []).find(q => q.id === interactionId);
|
|
145
|
+
if (question) {
|
|
146
|
+
interaction = { ...question, slideId: assessment.id };
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (interaction) {
|
|
153
|
+
await openEditor(interaction, slideId || interaction.slideId);
|
|
154
|
+
} else {
|
|
155
|
+
console.warn(`Interaction "${interactionId}" not found`);
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('Failed to open editor:', err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Close the editor modal
|
|
164
|
+
*/
|
|
165
|
+
export function closeEditor() {
|
|
166
|
+
const overlay = document.getElementById('stub-player-interaction-editor');
|
|
167
|
+
if (overlay) {
|
|
168
|
+
overlay.classList.remove('visible');
|
|
169
|
+
}
|
|
170
|
+
currentInteraction = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle save button click
|
|
175
|
+
*/
|
|
176
|
+
async function handleSave() {
|
|
177
|
+
if (!currentInteraction) return;
|
|
178
|
+
|
|
179
|
+
const overlay = document.getElementById('stub-player-interaction-editor');
|
|
180
|
+
const form = overlay?.querySelector('.edit-form');
|
|
181
|
+
|
|
182
|
+
if (!form) return;
|
|
183
|
+
|
|
184
|
+
const saveBtn = overlay.querySelector('.interaction-editor-save');
|
|
185
|
+
if (saveBtn) {
|
|
186
|
+
saveBtn.disabled = true;
|
|
187
|
+
saveBtn.textContent = 'Saving...';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const success = await saveItemEdits(
|
|
192
|
+
'/__edit-interaction',
|
|
193
|
+
form,
|
|
194
|
+
currentInteraction.slideId,
|
|
195
|
+
currentInteraction.id
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (success) {
|
|
199
|
+
closeEditor();
|
|
200
|
+
if (onSaveCallback) {
|
|
201
|
+
onSaveCallback();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
if (saveBtn) {
|
|
206
|
+
saveBtn.disabled = false;
|
|
207
|
+
saveBtn.textContent = '💾 Save';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|