From bd9c703213b20a31ea130108702ca87984afbe00 Mon Sep 17 00:00:00 2001 From: erseco Date: Wed, 29 Apr 2026 17:27:21 +0100 Subject: [PATCH] fix: show Edit button in popup/new/open/frame display modes For Display modes other than Embed, view.php delegated to exeweb_print_workaround() / exeweb_display_frame(), which never rendered the action_bar template. Teachers therefore had no Edit button and could only view the package. Render the action_bar in both code paths so the Edit (online or embedded editor) action is available in every Display mode. Also make the embedded editor modal attach to the topmost same-origin window. When the Edit button is clicked from inside a small frame (FRAME mode top frame, or any embedded iframe context), the overlay previously rendered inside the cramped frame and got stuck near the bottom with the close button out of reach. The modal now spans the full viewport. Refs #43 --- amd/build/editor_modal.min.js | 42 +++++++++++++++++++++++++++-------- amd/src/editor_modal.js | 42 +++++++++++++++++++++++++++-------- locallib.php | 5 +++++ 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/amd/build/editor_modal.min.js b/amd/build/editor_modal.min.js index 80635bb..17f9848 100644 --- a/amd/build/editor_modal.min.js +++ b/amd/build/editor_modal.min.js @@ -19,6 +19,20 @@ define(["exports", "core/str", "core/log"], function(exports, str, log) { var requestCounter = 0; var openAttemptCount = 0; var openResponseTimer = null; + var hostWindow = null; + var hostDocument = null; + + function getHostWindow() { + var candidate = window; + try { + while (candidate.parent && candidate.parent !== candidate && candidate.parent.document) { + candidate = candidate.parent; + } + } catch (e) { + // Cross-origin parent — stay where we are. + } + return candidate; + } var MAX_OPEN_ATTEMPTS = 3; var OPEN_RESPONSE_TIMEOUT_MS = 3000; @@ -106,7 +120,8 @@ define(["exports", "core/str", "core/log"], function(exports, str, log) { } function createLoadingModal() { - var modal = document.createElement("div"); + var targetDocument = hostDocument || document; + var modal = targetDocument.createElement("div"); modal.className = "exeweb-loading-modal"; modal.id = "exeweb-loading-modal"; @@ -123,7 +138,7 @@ define(["exports", "core/str", "core/log"], function(exports, str, log) { '

' + texts.saving + '

' + '

' + texts.wait + '

' + ''; - document.body.appendChild(modal); + targetDocument.body.appendChild(modal); return modal; }); } @@ -435,6 +450,8 @@ define(["exports", "core/str", "core/log"], function(exports, str, log) { var doClose = function() { var wasShowingLoader = isSaving || (skipConfirm === true); + var activeWindow = hostWindow || window; + var activeDocument = hostDocument || document; overlay.remove(); overlay = null; @@ -449,9 +466,11 @@ define(["exports", "core/str", "core/log"], function(exports, str, log) { openAttemptCount = 0; clearOpenResponseTimer(); - document.body.style.overflow = ""; - window.removeEventListener("message", handleMessage); - document.removeEventListener("keydown", handleKeydown); + activeDocument.body.style.overflow = ""; + activeWindow.removeEventListener("message", handleMessage); + activeDocument.removeEventListener("keydown", handleKeydown); + hostWindow = null; + hostDocument = null; if (wasShowingLoader) { setTimeout(function() { @@ -545,11 +564,16 @@ define(["exports", "core/str", "core/log"], function(exports, str, log) { }); overlay.appendChild(iframe); - document.body.appendChild(overlay); - document.body.style.overflow = "hidden"; + // Attach the overlay to the topmost same-origin window so the modal covers + // the full viewport even when triggered from inside a small frame (e.g. the + // FRAME display mode top frame, or the embedded iframe). + hostWindow = getHostWindow(); + hostDocument = hostWindow.document; + hostDocument.body.appendChild(overlay); + hostDocument.body.style.overflow = "hidden"; - window.addEventListener("message", handleMessage); - document.addEventListener("keydown", handleKeydown); + hostWindow.addEventListener("message", handleMessage); + hostDocument.addEventListener("keydown", handleKeydown); } function init() { diff --git a/amd/src/editor_modal.js b/amd/src/editor_modal.js index 5d9fdc9..e33d3fc 100644 --- a/amd/src/editor_modal.js +++ b/amd/src/editor_modal.js @@ -19,6 +19,20 @@ let session = null; let requestCounter = 0; let openAttemptCount = 0; let openResponseTimer = null; +let hostWindow = null; +let hostDocument = null; + +const getHostWindow = () => { + let candidate = window; + try { + while (candidate.parent && candidate.parent !== candidate && candidate.parent.document) { + candidate = candidate.parent; + } + } catch { + // Cross-origin parent — stay where we are. + } + return candidate; +}; const MAX_OPEN_ATTEMPTS = 3; const OPEN_RESPONSE_TIMEOUT_MS = 3000; @@ -105,7 +119,8 @@ const setSaveLabel = async(key, fallback) => { }; const createLoadingModal = async() => { - const modal = document.createElement('div'); + const targetDocument = hostDocument || document; + const modal = targetDocument.createElement('div'); modal.className = 'exeweb-loading-modal'; modal.id = 'exeweb-loading-modal'; @@ -126,7 +141,7 @@ const createLoadingModal = async() => { `; - document.body.appendChild(modal); + targetDocument.body.appendChild(modal); return modal; }; @@ -431,6 +446,8 @@ export const close = async(skipConfirm) => { } const wasShowingLoader = isSaving || (skipConfirm === true); + const activeWindow = hostWindow || window; + const activeDocument = hostDocument || document; overlay.remove(); overlay = null; @@ -445,9 +462,11 @@ export const close = async(skipConfirm) => { openAttemptCount = 0; clearOpenResponseTimer(); - document.body.style.overflow = ''; - window.removeEventListener('message', handleMessage); - document.removeEventListener('keydown', handleKeydown); + activeDocument.body.style.overflow = ''; + activeWindow.removeEventListener('message', handleMessage); + activeDocument.removeEventListener('keydown', handleKeydown); + hostWindow = null; + hostDocument = null; if (wasShowingLoader) { setTimeout(() => { @@ -527,11 +546,16 @@ export const open = async(cmid, editorUrl, activityName, packageUrl, saveUrl, se }); overlay.appendChild(iframe); - document.body.appendChild(overlay); - document.body.style.overflow = 'hidden'; + // Attach the overlay to the topmost same-origin window so the modal covers + // the full viewport even when triggered from inside a small frame (e.g. the + // FRAME display mode top frame, or the embedded iframe). + hostWindow = getHostWindow(); + hostDocument = hostWindow.document; + hostDocument.body.appendChild(overlay); + hostDocument.body.style.overflow = 'hidden'; - window.addEventListener('message', handleMessage); - document.addEventListener('keydown', handleKeydown); + hostWindow.addEventListener('message', handleMessage); + hostDocument.addEventListener('keydown', handleKeydown); }; export const init = () => { diff --git a/locallib.php b/locallib.php index c0d44c6..633e803 100644 --- a/locallib.php +++ b/locallib.php @@ -87,6 +87,8 @@ function exeweb_display_frame($exeweb, $cm, $course, $file) { $PAGE->set_pagelayout('frametop'); $PAGE->activityheader->set_description(exeweb_get_intro($exeweb, $cm, true)); exeweb_print_header($exeweb, $cm, $course); + // Show action bar with Edit button in the top frame. + echo $PAGE->get_renderer('mod_exeweb')->generate_action_bar($cm); if (!exeweb_is_teacher_mode_visible($exeweb)) { exeweb_require_teacher_mode_hider_for_content_frame(); } @@ -218,6 +220,9 @@ function exeweb_print_workaround($exeweb, $cm, $course, $file) { exeweb_print_header($exeweb, $cm, $course); + // Show action bar with Edit button when user has edit capability. + echo $PAGE->get_renderer('mod_exeweb')->generate_action_bar($cm); + echo '
'; switch ($exeweb->display) { case RESOURCELIB_DISPLAY_POPUP: