From 20700a0b9be6f6750b57d434d64bf394dd3930f0 Mon Sep 17 00:00:00 2001 From: erseco Date: Sat, 26 Sep 2026 11:17:08 +0100 Subject: [PATCH 1/4] fix: serve embed behavior as an enqueued script instead of inline markup WordPress.org asked that plugins stop printing - ', $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: build our tags directly. +$exelearning_integration_assets = wp_get_inline_script_tag( $exelearning_wp_config_script ) . wp_get_script_tag( + array( 'src' => add_query_arg( 'ver', EXELEARNING_VERSION, $exelearning_plugin_assets_url . '/js/wp-exe-bridge.js' ) ) +); +wp_register_style( 'exelearning-editor-page', false, array(), EXELEARNING_VERSION ); +wp_add_inline_style( 'exelearning-editor-page', $exelearning_page_styles ); +ob_start(); +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..6c8de82 100644 --- a/includes/class-elp-upload-block.php +++ b/includes/class-elp-upload-block.php @@ -49,6 +49,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 +364,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 .= '
'; $html .= sprintf( '', $iframe_src_attr, - esc_attr( $height ), + esc_attr( $this->height_style( $height ) ), $is_poster ? ' display: none;' : '', esc_attr( $title ) ); return sprintf( - '
+ '
%2$s
@@ -386,104 +386,36 @@ class="exelearning-iframe"
%5$s %6$s -
%7$s', +
', esc_attr( $unique_id ), esc_html( $title ), '' !== $download_html ? $download_html : $fallback_download, $fullscreen_html, $poster_html, $iframe_html, - $this->render_preview_script( $unique_id, $is_poster, $fullscreen ), esc_attr( $width ) ); } /** - * Build the inline behavior script for a preview iframe. - * - * Wires only the behaviors present in this instance: the optional fullscreen - * button and the optional poster click-to-load. When both are enabled the - * fullscreen button first activates the deferred poster (loading and - * revealing the iframe) so it never tries to expand a hidden, srcless frame. - * When neither is enabled no script is emitted. Each block is scoped to the - * instance container so multiple embeds on one page stay independent. + * CSS sizing declarations for an embed of the requested height. * - * Teacher-mode visibility is handled by eXeLearning core through the - * ?exe-teacher=1 query parameter on the iframe src, so no host-side CSS/JS - * injection is emitted here. + * A pixel height is written as-is. A percentage means "this fraction of the + * rendered width" -- the reading every eXeLearning embed has always had -- and + * CSS `height: 75%` does not say that: it resolves against the parent's height, + * which no theme sets, so the frame collapses. `aspect-ratio` states the same + * intent in CSS alone, which is why the script and resize observer this used to + * need are gone. * - * @param string $unique_id Container element ID. - * @param bool $is_poster Whether the iframe loads lazily from a poster. - * @param bool $fullscreen Whether the fullscreen button is present. - * @return string Inline ', - esc_attr( $unique_id ), - $body - ); + return sprintf( 'height: %s;', $height ); } /** @@ -518,6 +450,9 @@ private function render_toolbar_download_fallback( $file_url ) { private function enqueue_frontend_assets() { wp_enqueue_style( 'dashicons' ); + // Registered with the block's frontend assets. + wp_enqueue_script( 'exelearning-embed' ); + wp_enqueue_style( 'exelearning-frontend', plugins_url( '../assets/css/exelearning.css', __FILE__ ), diff --git a/tests/e2e/shortcode-viewer.spec.js b/tests/e2e/shortcode-viewer.spec.js index a133c46..779cc48 100644 --- a/tests/e2e/shortcode-viewer.spec.js +++ b/tests/e2e/shortcode-viewer.spec.js @@ -190,12 +190,13 @@ test.describe('Shortcode viewer (public frontend)', () => { await expect(page.locator('.exelearning-fullscreen-btn')).toBeVisible(); }); - test('height="75%" is applied to the iframe', async ({ page }) => { + test('height="75%" makes the iframe 75% as tall as it is wide', async ({ page }) => { await gotoScenario(page, 'height'); - // A percentage height can compute to 0 without a sized parent, so assert - // on the inline style rather than the element's rendered box. - const style = await page.locator('iframe.exelearning-iframe').getAttribute('style'); - expect(style).toContain('height: 75%'); + // The percentage is rendered as an aspect-ratio, which resolves against the + // embed's own width, so the rendered box is what this can be asserted on -- + // a bare CSS `height: 75%` would have computed to 0 against an unsized parent. + const box = await page.locator('iframe.exelearning-iframe').boundingBox(); + expect(Math.abs(box.height - box.width * 0.75)).toBeLessThanOrEqual(2); }); test('width="75%" is applied to the embed box', async ({ page }) => { diff --git a/tests/js/exelearning_embed.test.js b/tests/js/exelearning_embed.test.js new file mode 100644 index 0000000..74f01dc --- /dev/null +++ b/tests/js/exelearning_embed.test.js @@ -0,0 +1,206 @@ +// Unit tests for assets/js/exelearning-embed.js. +// +// The script replaces the per-instance inline