diff --git a/admin/views/editor-bootstrap.php b/admin/views/editor-bootstrap.php
index 81b62a2..d9d2050 100644
--- a/admin/views/editor-bootstrap.php
+++ b/admin/views/editor-bootstrap.php
@@ -109,11 +109,8 @@
);
// Inject WordPress configuration BEFORE the closing tag.
-// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Standalone HTML page output, not a WordPress template.
$exelearning_wp_config_script = sprintf(
'
-
-
-
',
$exelearning_attachment_id,
wp_json_encode( $exelearning_elp_url ),
@@ -478,15 +473,11 @@ function normalizeEditorAssetUrl(url) {
$exelearning_user_id,
wp_json_encode( $exelearning_editor_base_url ),
wp_json_encode( $exelearning_i18n ),
- wp_json_encode( $exelearning_theme_registry_override ),
- esc_url( $exelearning_plugin_assets_url )
+ wp_json_encode( $exelearning_theme_registry_override )
);
-// phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
// WordPress-specific styles.
$exelearning_page_styles = '
-
-
';
-// Insert config script and styles before .
-$exelearning_template = str_replace( '', $exelearning_wp_config_script . $exelearning_page_styles . '', $exelearning_template );
+// Print only our handles into the standalone document, without a theme header.
+wp_enqueue_script( 'exelearning-editor-bridge', $exelearning_plugin_assets_url . '/js/wp-exe-bridge.js', array(), EXELEARNING_VERSION, false );
+wp_add_inline_script( 'exelearning-editor-bridge', $exelearning_wp_config_script, 'before' );
+wp_register_style( 'exelearning-editor-page', false, array(), EXELEARNING_VERSION );
+wp_enqueue_style( 'exelearning-editor-page' );
+wp_add_inline_style( 'exelearning-editor-page', $exelearning_page_styles );
+ob_start();
+wp_print_scripts( array( 'exelearning-editor-bridge' ) );
+wp_print_styles( array( 'exelearning-editor-page' ) );
+$exelearning_integration_assets = ob_get_clean();
+$exelearning_template = str_replace( '', $exelearning_integration_assets . '', $exelearning_template );
// Add tag to set the base URL for all relative paths.
// This ensures paths like "files/perm/..." resolve to the static editor directory.
diff --git a/assets/js/exelearning-embed.js b/assets/js/exelearning-embed.js
new file mode 100644
index 0000000..9aa6116
--- /dev/null
+++ b/assets/js/exelearning-embed.js
@@ -0,0 +1,85 @@
+/**
+ * Toolbar and poster behavior for embedded eXeLearning packages.
+ *
+ * One enqueued script serves shortcode and block controls, including embeds
+ * inserted after load. Each click stays within its own embed container.
+ */
+( function () {
+ 'use strict';
+
+ /**
+ * Promote a deferred frame: load it, reveal it and drop the poster.
+ *
+ * In poster mode the iframe ships with its URL in `data-src` and hidden, so the
+ * package is downloaded only when the visitor asks for it. Called both by the
+ * poster itself and by the fullscreen button, which must not expand a hidden
+ * frame that has no document yet.
+ *
+ * @param {Element} container The embed.
+ * @return {void}
+ */
+ function activate( container ) {
+ var iframe = container.querySelector( '.exelearning-iframe' );
+ if ( ! iframe ) {
+ return;
+ }
+
+ var deferred = iframe.getAttribute( 'data-src' );
+ if ( deferred && ! iframe.getAttribute( 'src' ) ) {
+ iframe.setAttribute( 'src', deferred );
+ }
+
+ iframe.style.display = '';
+
+ var poster = container.querySelector( '.exelearning-poster' );
+ if ( poster ) {
+ poster.style.display = 'none';
+ }
+ }
+
+ /**
+ * Take the embed frame fullscreen, whatever the browser calls it.
+ *
+ * @param {Element} container The embed.
+ * @return {void}
+ */
+ function fullscreen( container ) {
+ var iframe = container.querySelector( '.exelearning-iframe' );
+ if ( ! iframe ) {
+ return;
+ }
+
+ if ( iframe.requestFullscreen ) {
+ iframe.requestFullscreen();
+ } else if ( iframe.webkitRequestFullscreen ) {
+ iframe.webkitRequestFullscreen();
+ } else if ( iframe.msRequestFullscreen ) {
+ iframe.msRequestFullscreen();
+ }
+ }
+
+ document.addEventListener( 'click', function ( event ) {
+ var target = event.target;
+ if ( ! target || ! target.closest ) {
+ return;
+ }
+
+ var control = target.closest( '.exelearning-poster, .exelearning-fullscreen-btn' );
+ if ( ! control ) {
+ return;
+ }
+
+ var container = control.closest( '.exelearning-preview, .exelearning-block-frontend' );
+ if ( ! container ) {
+ return;
+ }
+
+ // The fullscreen button in poster mode loads and reveals the frame first:
+ // expanding a hidden, srcless frame would fill the screen with nothing.
+ activate( container );
+
+ if ( control.classList.contains( 'exelearning-fullscreen-btn' ) ) {
+ fullscreen( container );
+ }
+ } );
+}() );
diff --git a/docs/SHORTCODES.md b/docs/SHORTCODES.md
index 210b506..e26b2a7 100644
--- a/docs/SHORTCODES.md
+++ b/docs/SHORTCODES.md
@@ -90,8 +90,9 @@ content keeps working.
`height` applies to the preview iframe (and screenshot poster). Pixel values
remain fixed, while percentage values define a responsive height relative to the
rendered embed width. For example, `height="75%"` produces a 4:3 preview and
-`height="100%"` produces a square preview. The height is recalculated whenever
-the embed width changes.
+`height="100%"` produces a square preview. It is rendered as a CSS
+`aspect-ratio`, so the browser keeps the proportion on every resize with no
+script involved.
Anything outside the two supported forms (`0`, negative numbers, `calc()`,
`var()`, viewport units such as `100vh`, or attempted injections) is rejected and
diff --git a/exelearning.php b/exelearning.php
index df721cf..efb2ed6 100644
--- a/exelearning.php
+++ b/exelearning.php
@@ -51,7 +51,7 @@
require_once EXELEARNING_PLUGIN_DIR . 'includes/class-download-formats.php';
require_once EXELEARNING_PLUGIN_DIR . 'includes/class-download-button-renderer.php';
require_once EXELEARNING_PLUGIN_DIR . 'includes/class-elp-upload-block.php';
-require_once EXELEARNING_PLUGIN_DIR . 'includes/class-viewer-enhancements.php';
+require_once EXELEARNING_PLUGIN_DIR . 'includes/class-embed-assets.php';
// Styles management (uploaded/builtin registry).
require_once EXELEARNING_PLUGIN_DIR . 'includes/class-style-package.php';
@@ -100,9 +100,6 @@
* Starts the plugin.
*/
function exelearning_run() {
- $viewer_enhancements = new ExeLearning_Viewer_Enhancements();
- $viewer_enhancements->register_hooks();
-
$plugin = new ExeLearning();
$plugin->run();
}
diff --git a/includes/class-elp-upload-block.php b/includes/class-elp-upload-block.php
index f87e19d..b9b917c 100644
--- a/includes/class-elp-upload-block.php
+++ b/includes/class-elp-upload-block.php
@@ -356,6 +356,10 @@ private function render_block_preview( $data, $download_html ) {
// prints; in a REST render the handle was never registered and this is a
// harmless no-op.
wp_enqueue_script( 'exelearning-embed-loader' );
+
+ // Drives the fullscreen button, shared with the shortcode. Enqueued here,
+ // at the point the markup it binds is rendered, for the same reason.
+ ExeLearning_Embed_Assets::enqueue();
$html .= '