From 75017ed29571d25fdd0524b48122ffee6a1d2418 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Tue, 18 Nov 2025 14:36:12 -0500 Subject: [PATCH 01/32] feat: Add overlay option for image node --- branching_xblock/branching_xblock.py | 49 +++++++++++++++++-- .../static/css/branching_xblock.css | 44 +++++++++++++++++ branching_xblock/static/css/studio_editor.css | 15 ++++++ .../static/handlebars/node-block.handlebars | 6 +++ .../static/js/src/branching_xblock.js | 22 +++++++-- .../static/js/src/studio_editor.js | 38 +++++++++++++- 6 files changed, 166 insertions(+), 8 deletions(-) diff --git a/branching_xblock/branching_xblock.py b/branching_xblock/branching_xblock.py index 5f10458..75238af 100644 --- a/branching_xblock/branching_xblock.py +++ b/branching_xblock/branching_xblock.py @@ -1,5 +1,6 @@ """Branching Scenario XBlock.""" import json +import logging import os import uuid from typing import Any, Optional @@ -13,6 +14,8 @@ resource_loader = ResourceLoader(__name__) +logger = logging.getLogger(__name__) + class BranchingXBlock(XBlock): """ @@ -114,7 +117,14 @@ def get_node(self, node_id): """ Get a node by its ID. """ - return self.scenario_data.get("nodes", {}).get(node_id) + node = self.scenario_data.get("nodes", {}).get(node_id) + if node is None: + return None + if "overlay_text" not in node: + # ensure older scenarios expose the flag downstream + node = {**node, "overlay_text": False} + self.scenario_data.setdefault("nodes", {})[node_id] = node + return node def get_current_node(self) -> Optional[dict[str, Any]]: """ @@ -129,6 +139,27 @@ def is_end_node(self, node_id): node = self.get_node(node_id) return bool(node) and not node.get("choices") + def get_choice(self, node, choice_index): + """ + Validate and return a choice from a node. + """ + try: + return node["choices"][choice_index] + except (IndexError, KeyError, TypeError): + return None + + def can_undo(self): + """ + Check if undo is allowed and possible. + """ + return self.enable_undo and len(self.history) > 0 + + def get_previous_node_id(self): + """ + Get last node from history. + """ + return self.history[-1] if self.history else None + def validate_scenario(self): """ Check for common configuration errors. @@ -211,8 +242,7 @@ def studio_view(self, context=None): get_site_configuration_value("branching_xblock", "AUTHORING_HELP_HTML") or "" ) init_data = { - "nodes": self.scenario_data.get("nodes", {}), - "start_node_id": self.scenario_data.get("start_node_id"), + "nodes": self.scenario_data.get("nodes", []), "enable_undo": bool(self.enable_undo), "enable_scoring": bool(self.enable_scoring), "enable_hints": bool(self.enable_hints), @@ -225,8 +255,17 @@ def studio_view(self, context=None): return frag def _get_state(self): + nodes = self.scenario_data.get("nodes", {}) + nodes_with_defaults = { + node_id: { + **node, + "overlay_text": bool(node.get("overlay_text", False)), + } + for node_id, node in nodes.items() + } + return { - "nodes": self.scenario_data.get("nodes", {}), + "nodes": nodes_with_defaults, "start_node_id": self.scenario_data.get("start_node_id"), "enable_undo": bool(self.enable_undo), "enable_scoring": bool(self.enable_scoring), @@ -323,6 +362,7 @@ def studio_submit(self, data, suffix=''): }, 'choices': raw.get('choices', []), 'hint': raw.get('hint', ''), + 'overlay_text': bool(raw.get('overlay_text', False)), 'transcript_url': raw.get('transcript_url', ''), }) @@ -359,6 +399,7 @@ def studio_submit(self, data, suffix=''): 'media': node['media'], 'choices': cleaned, 'hint': node.get('hint', ''), + 'overlay_text': bool(node.get('overlay_text', False)), 'transcript_url': node.get('transcript_url', ''), }) diff --git a/branching_xblock/static/css/branching_xblock.css b/branching_xblock/static/css/branching_xblock.css index 4322065..73bd075 100644 --- a/branching_xblock/static/css/branching_xblock.css +++ b/branching_xblock/static/css/branching_xblock.css @@ -26,6 +26,50 @@ align-self: center; } +.branching-scenario .media-overlay { + position: relative; + width: 100%; + max-width: 100%; + display: inline-block; +} + +.branching-scenario .media-overlay img { + width: 100%; + height: auto; + display: block; + object-fit: cover; + border-radius: 0.5rem; +} + +.branching-scenario .media-overlay__text { + position: absolute; + left: 50%; + bottom: 1.25rem; + transform: translateX(-50%); + width: calc(100% - 3rem); + max-width: 90%; + background: rgba(8, 36, 73, 0.85); + color: #fff; + padding: 1rem 1.5rem; + border-radius: 0.5rem; + font-size: 1.05rem; + line-height: 1.5; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); +} + +.branching-scenario .media-overlay__text p { + margin: 0; +} + +@media (max-width: 768px) { + .branching-scenario .media-overlay__text { + position: static; + transform: none; + width: 100%; + margin-top: 0.75rem; + } +} + .branching-scenario .node-media img, .branching-scenario .node-media video, .branching-scenario .node-media iframe { diff --git a/branching_xblock/static/css/studio_editor.css b/branching_xblock/static/css/studio_editor.css index c907de3..9528af0 100644 --- a/branching_xblock/static/css/studio_editor.css +++ b/branching_xblock/static/css/studio_editor.css @@ -239,3 +239,18 @@ margin-top: 0.25em; font-style: italic; } + +.overlay-text-control { + margin-bottom: 1em; +} + +.overlay-text-control.is-hidden { + display: none; +} + +.overlay-text-toggle { + display: flex; + align-items: center; + gap: 0.5em; + font-weight: 600; +} diff --git a/branching_xblock/static/handlebars/node-block.handlebars b/branching_xblock/static/handlebars/node-block.handlebars index 7e74891..661f816 100644 --- a/branching_xblock/static/handlebars/node-block.handlebars +++ b/branching_xblock/static/handlebars/node-block.handlebars @@ -22,6 +22,12 @@ +
+ +
diff --git a/branching_xblock/static/js/src/branching_xblock.js b/branching_xblock/static/js/src/branching_xblock.js index 5d3f82f..34611c4 100644 --- a/branching_xblock/static/js/src/branching_xblock.js +++ b/branching_xblock/static/js/src/branching_xblock.js @@ -89,6 +89,9 @@ function BranchingXBlock(runtime, element) { const media = (node && node.media) || {}; const mediaUrl = media.url || ''; + const contentHtml = (node && node.content) || ''; + const overlayEnabled = Boolean(node?.overlay_text && media.type === 'image'); + const $media = $el.find('[data-role="media"]'); const $transcript = $el.find('[data-role="transcript"]'); const setTranscript = (href) => { @@ -98,7 +101,16 @@ function BranchingXBlock(runtime, element) { $transcript.prop('hidden', true).empty(); } }; - if (media.type === 'image') { + if (media.type === 'image' && overlayEnabled) { + $media.html(` +
+ +
+ ${contentHtml} +
+
+ `); + } else if (media.type === 'image') { $media.html(``); setTranscript(null); } else if (media.type === 'audio') { @@ -121,8 +133,12 @@ function BranchingXBlock(runtime, element) { setTranscript(null); } // Content - const nodeContent = (node && node.content) || ''; - $el.find('[data-role="content"]').html(nodeContent); + const $content = $el.find('[data-role="content"]'); + if (overlayEnabled) { + $content.empty(); + } else { + $content.html(contentHtml); + } // Hint const nodeId = node?.id || null; diff --git a/branching_xblock/static/js/src/studio_editor.js b/branching_xblock/static/js/src/studio_editor.js index f8de786..daec06f 100644 --- a/branching_xblock/static/js/src/studio_editor.js +++ b/branching_xblock/static/js/src/studio_editor.js @@ -19,6 +19,16 @@ function BranchingStudioEditor(runtime, element, data) { return `temp-${i}`; }; + function loadState() { + return $.ajax({ + type: 'POST', + url: runtime.handlerUrl(element, 'get_current_state'), + data: '{}', + contentType: 'application/json; charset=utf-8', + dataType: 'json' + }); + } + function render(state) { $editor.empty(); $errors.empty(); @@ -31,6 +41,7 @@ function BranchingStudioEditor(runtime, element, data) { media: {type: '', url: ''}, choices: [], hint: '', + overlay_text: false, transcript_url: '' }); } @@ -38,6 +49,7 @@ function BranchingStudioEditor(runtime, element, data) { $editor.append(Templates['settings-panel'](state)); nodes.forEach((node, idx) => { + const options = nodes.map((n, j) => ({ id: n.id, label: `Node ${j+1}` @@ -118,6 +130,18 @@ function BranchingStudioEditor(runtime, element, data) { }); } + function toggleOverlayControl($node) { + const type = $node.find('.media-type').val(); + const $control = $node.find('.overlay-text-control'); + const $checkbox = $node.find('.overlay-text-checkbox'); + if (type === 'image') { + $control.removeClass('is-hidden'); + } else { + $control.addClass('is-hidden'); + $checkbox.prop('checked', false); + } + } + function bindInteractions() { $editor.find('.btn-delete-node').off('click').on('click', function() { $(this).closest('.node-block').remove(); @@ -161,6 +185,7 @@ function BranchingStudioEditor(runtime, element, data) { media: { type: '', url: '' }, choices: [], hint: '', + overlay_text: false, transcript_url: '' }, idx @@ -172,6 +197,15 @@ function BranchingStudioEditor(runtime, element, data) { updateChoiceUI(); updateTranscriptVisibility($newNode); }); + + $editor.find('.media-type').off('change.overlay').on('change.overlay', function() { + const $node = $(this).closest('.node-block'); + toggleOverlayControl($node); + }); + + $editor.find('.node-block').each(function() { + toggleOverlayControl($(this)); + }); } function bindActions() { @@ -195,6 +229,7 @@ function BranchingStudioEditor(runtime, element, data) { const mediaType = $n.find('.media-type').val(); const choices = []; const nodeHint = $n.find('.node-hint').val()?.trim() || ''; + const overlayEnabled = $n.find('.overlay-text-checkbox').is(':checked'); const transcriptUrl = $n.find('.transcript-url').val()?.trim() || ''; $n.find('.choice-row').each(function() { @@ -212,6 +247,7 @@ function BranchingStudioEditor(runtime, element, data) { media: { type: mediaType, url: mediaUrl }, choices, hint: nodeHint, + overlay_text: overlayEnabled, transcript_url: transcriptUrl }); } @@ -243,5 +279,5 @@ function BranchingStudioEditor(runtime, element, data) { }); } - render(data || {}); + loadState().then(render); } From 06e40d3209259a73323ef4176c3b2b6bd6a5aff4 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Tue, 18 Nov 2025 15:15:56 -0500 Subject: [PATCH 02/32] refactor: change choices to radio single choice --- .../static/css/branching_xblock.css | 122 +++++++++++++----- branching_xblock/static/css/studio_editor.css | 6 + .../static/handlebars/node-block.handlebars | 1 + .../static/html/branching_xblock.html | 19 ++- .../static/js/src/branching_xblock.js | 66 +++++++--- 5 files changed, 156 insertions(+), 58 deletions(-) diff --git a/branching_xblock/static/css/branching_xblock.css b/branching_xblock/static/css/branching_xblock.css index 73bd075..282eaf8 100644 --- a/branching_xblock/static/css/branching_xblock.css +++ b/branching_xblock/static/css/branching_xblock.css @@ -182,66 +182,118 @@ .branching-scenario .choices { + padding: 1.5em 1em; +} + +.branching-scenario .choices-heading { + font-size: 1.15rem; + font-weight: 600; + margin-bottom: 1rem; + color: #1f2a37; +} + +.branching-scenario .choices-form { display: flex; - flex-wrap: wrap; - gap: 0.75em; - padding: 1em; - justify-content: flex-start; + flex-direction: column; + gap: 1rem; } -/* inline mode keeps the natural width */ -.branching-scenario .choices-inline { - flex-wrap: wrap; +.branching-scenario .choices-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.branching-scenario .choice-option { + display: flex; + align-items: flex-start; + gap: 0.75rem; + border: 1px solid #d6d6d6; + border-radius: 8px; + padding: 0.9rem 1rem; + background-color: #fff; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; + cursor: pointer; +} + +.branching-scenario .choice-option:hover, +.branching-scenario .choice-option:focus-within { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); +} + +.branching-scenario .choice-option.is-selected { + border-color: #0f62a1; + box-shadow: 0 0 0 2px rgba(15, 98, 161, 0.25); + background-color: #f0f7ff; +} + +.branching-scenario .choice-option__input { + margin-top: 0.2rem; + width: 1.1rem; + height: 1.1rem; +} + +.branching-scenario .choice-option__text { + flex: 1; + font-size: 1rem; + color: #1f2a37; +} + +.branching-scenario .choice-actions { + display: flex; justify-content: space-between; align-items: center; + gap: 1rem; + flex-wrap: wrap; } -.branching-scenario .choices .choice-button { +.choice-submit-button { + border: none; + background-color: #0f62a1; color: #fff; - border: 1px solid #0e6aa6; border-radius: 6px; - padding: 0.65em 1.4em; + padding: 0.7rem 1.5rem; font-size: 1rem; - font-weight: 400; + font-weight: 600; cursor: pointer; - background-color: #0075b4; - background-image: none !important; - box-shadow: none !important; - display: inline-flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.35em; - line-height: 1.3; - white-space: normal; - text-align: center; - min-width: 6rem; - max-width: 100%; - flex: 0 1 18rem; + transition: background-color 0.2s ease; } -.branching-scenario .choices-inline .choice-button { - flex: 0 0 auto; +.choice-submit-button:disabled { + opacity: 0.5; + cursor: not-allowed; } -.branching-scenario .choices .choice-button:hover, -.branching-scenario .choices .choice-button:focus { - background-color: #005f94; - border-color: #0a5278; - text-decoration: none; +.choice-submit-button:not(:disabled):hover, +.choice-submit-button:not(:disabled):focus { + background-color: #0c4f83; } .undo-button { - border: none; + border: 1px solid #4a5568; border-radius: 4px; padding: 0.6em 1.2em; font-size: 1em; cursor: pointer; - background-image: none !important; - margin-left: 1em; + background: transparent; + color: #1f2a37; } .score-display { padding: 1em 1em; font-size: small; } + +@media (max-width: 600px) { + .branching-scenario .choice-actions { + flex-direction: column-reverse; + align-items: stretch; + } + + .undo-button, + .choice-submit-button { + width: 100%; + text-align: center; + } +} diff --git a/branching_xblock/static/css/studio_editor.css b/branching_xblock/static/css/studio_editor.css index 9528af0..1c27505 100644 --- a/branching_xblock/static/css/studio_editor.css +++ b/branching_xblock/static/css/studio_editor.css @@ -254,3 +254,9 @@ gap: 0.5em; font-weight: 600; } + +.overlay-text-help { + margin: 0.5em 0 0; + font-size: 0.85em; + color: #555; +} diff --git a/branching_xblock/static/handlebars/node-block.handlebars b/branching_xblock/static/handlebars/node-block.handlebars index 661f816..9652af2 100644 --- a/branching_xblock/static/handlebars/node-block.handlebars +++ b/branching_xblock/static/handlebars/node-block.handlebars @@ -27,6 +27,7 @@ Overlay text on image +

If left unchecked, text will appear outside the image.