diff --git a/AGENTS.md b/AGENTS.md
index f94b692..05be42a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,7 +26,7 @@ Packages are plain WordPress **attachments** — there is no custom post type.
| Ingestion | `includes/class-elp-upload-handler.php` (upload, extraction, delete cleanup), `includes/class-elp-file-service.php` |
| Reprocessing | `includes/class-elp-reprocessor.php` (class `ExeLearning_Reprocessor`), used by REST save/reprocess, the Media Library bulk action and `wp exelearning reprocess` (`includes/class-cli-command.php`); `includes/class-content-hash-aliases.php` keeps retired hashes as redirects (ADR-68-01) |
| Content delivery | `includes/class-content-proxy.php`: `GET /wp-json/exelearning/v1/content/{hash}/{file}` with security headers and CSP. A generated `.htaccess` blocks direct HTML/SVG/XML access under `uploads/exelearning/` (Apache only); the `exelearning_content_origin` filter serves content from a separate host |
-| Frontend | `public/class-shortcodes.php`, `includes/class-elp-upload-block.php` (Block API v3), `includes/class-viewer-enhancements.php` (shortcode only), `includes/class-download-button-renderer.php` + `includes/class-export-bootstrap.php` (exports through the editor in a hidden `?exe_export=1` iframe) |
+| Frontend | `public/class-shortcodes.php`, `includes/class-elp-upload-block.php` (Block API v3), `assets/js/exelearning-embed.js` (fullscreen and poster behavior shared by shortcode and block, enqueued only when an embed renders), `includes/class-download-button-renderer.php` + `includes/class-export-bootstrap.php` (exports through the editor in a hidden `?exe_export=1` iframe) |
| Admin | `admin/class-admin-settings.php`, `includes/integrations/class-media-library.php` (columns, meta boxes, previews, bulk reprocess), styles: `admin/class-admin-styles.php`, `includes/class-styles-service.php`, `includes/class-style-package.php` (`uploads/exelearning-styles/{slug}/`) |
| Embedded editor | `includes/class-exelearning-editor.php`, `admin/views/editor-bootstrap.php` (loads `dist/static/index.html` with WordPress config), `assets/js/exelearning-editor.js` (modal) ↔ postMessage ↔ `assets/js/wp-exe-bridge.js` (inside the editor iframe), `includes/class-editor-bundle.php` |
diff --git a/admin/views/editor-bootstrap.php b/admin/views/editor-bootstrap.php
index 81b62a2..907b258 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 );
+// Standalone document without a theme header: print only our handles, with
+// the config inline before the bridge.
+wp_register_script( 'exelearning-editor-bridge', $exelearning_plugin_assets_url . '/js/wp-exe-bridge.js', array(), EXELEARNING_VERSION, false );
+wp_enqueue_script( 'exelearning-editor-bridge' );
+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/exelearning.php b/exelearning.php
index 0c0a39b..d9fcb2d 100644
--- a/exelearning.php
+++ b/exelearning.php
@@ -48,7 +48,6 @@
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';
// Styles management (uploaded/builtin registry).
require_once EXELEARNING_PLUGIN_DIR . 'includes/class-style-package.php';
@@ -96,9 +95,6 @@
* Starts the plugin.
*/
function exelearning_run() {
- $viewer_enhancements = new ExeLearning_Viewer_Enhancements();
- $viewer_enhancements->register_hooks();
-
new ExeLearning();
}
diff --git a/includes/class-elp-upload-block.php b/includes/class-elp-upload-block.php
index 7cde2b6..ba73525 100644
--- a/includes/class-elp-upload-block.php
+++ b/includes/class-elp-upload-block.php
@@ -21,17 +21,13 @@ class ExeLearning_Elp_Upload_Block {
*/
public function __construct() {
add_action( 'init', array( $this, 'register_block' ) );
+ add_action( 'init', array( $this, 'register_frontend_scripts' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_scripts' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) );
}
/**
- * Enqueue frontend styles and register the shared embed-loader behavior.
- *
- * The loader is registered here but not enqueued: a page with no eXeLearning
- * block has nothing for it to bind, and enqueueing from this hook would put the
- * request on every frontend page of the site. render_block_preview() enqueues it
- * at the point it emits a wrapper for the loader to find.
+ * Enqueue frontend styles.
*/
public function enqueue_frontend_styles() {
wp_enqueue_style(
@@ -40,6 +36,17 @@ public function enqueue_frontend_styles() {
array(),
EXELEARNING_VERSION
);
+ }
+
+ /**
+ * Register the embed scripts on init, so every context that renders an embed
+ * (theme, /embed/ template, previews, admin) can enqueue them by handle.
+ *
+ * They are registered but not enqueued: a page with no eXeLearning embed has
+ * nothing for them to bind. The block and the shortcode enqueue them at the
+ * point they render the markup the scripts look for.
+ */
+ public function register_frontend_scripts() {
// Binds every `.exelearning-embed-loader` on the page at once, so the
// spinner costs one cached file instead of an inline copy per block.
wp_register_script(
@@ -49,6 +56,14 @@ public function enqueue_frontend_styles() {
EXELEARNING_VERSION,
true
);
+ // Fullscreen and click-to-load poster for every embed, block or shortcode.
+ wp_register_script(
+ 'exelearning-embed',
+ plugins_url( '../assets/js/exelearning-embed.js', __FILE__ ),
+ array(),
+ EXELEARNING_VERSION,
+ true
+ );
}
/**
@@ -356,6 +371,8 @@ 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' );
+
+ wp_enqueue_script( 'exelearning-embed' );
$html .= '