Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down
27 changes: 14 additions & 13 deletions admin/views/editor-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,8 @@
);

// Inject WordPress configuration BEFORE the closing </head> tag.
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Standalone HTML page output, not a WordPress template.
$exelearning_wp_config_script = sprintf(
'
<!-- WordPress Integration Configuration -->
<script>
// WordPress Integration Configuration
window.__WP_EXE_CONFIG__ = {
mode: "WordPress",
Expand Down Expand Up @@ -465,8 +462,6 @@ function normalizeEditorAssetUrl(url) {
});
}
})();
</script>
<script src="%s/js/wp-exe-bridge.js"></script>
',
$exelearning_attachment_id,
wp_json_encode( $exelearning_elp_url ),
Expand All @@ -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 = '
<!-- WordPress-specific styles -->
<style>
/* WordPress-specific overrides */
html, body {
height: 100%;
Expand Down Expand Up @@ -529,11 +520,21 @@ function normalizeEditorAssetUrl(url) {
#mobile-navbar-button-openuserodefiles {
display: none !important;
}
</style>
';

// Insert config script and styles before </head>.
$exelearning_template = str_replace( '</head>', $exelearning_wp_config_script . $exelearning_page_styles . '</head>', $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( '</head>', $exelearning_integration_assets . '</head>', $exelearning_template );

// Add <base> tag to set the base URL for all relative paths.
// This ensures paths like "files/perm/..." resolve to the static editor directory.
Expand Down
85 changes: 85 additions & 0 deletions assets/js/exelearning-embed.js
Original file line number Diff line number Diff line change
@@ -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 );
}
} );
}() );
4 changes: 0 additions & 4 deletions exelearning.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -96,9 +95,6 @@
* Starts the plugin.
*/
function exelearning_run() {
$viewer_enhancements = new ExeLearning_Viewer_Enhancements();
$viewer_enhancements->register_hooks();

new ExeLearning();
}

Expand Down
70 changes: 23 additions & 47 deletions includes/class-elp-upload-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
);
}

/**
Expand Down Expand Up @@ -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 .= '<div class="exelearning-embed-loader">';
$html .= sprintf(
'<iframe
Expand All @@ -379,47 +396,6 @@ class="exelearning-iframe"

$html .= '</div>';

if ( ! empty( $data['fullscreen'] ) ) {
$html .= $this->render_block_fullscreen_script( $data['container_id'] );
}

return $html;
}

/**
* Build the inline fullscreen behavior script for a block preview.
*
* Scoped to the instance container so multiple blocks on one page stay
* independent. The button fullscreens the iframe element from the parent
* page, which works regardless of the iframe sandbox.
*
* @param string $container_id Container element ID.
* @return string Inline <script> markup.
*/
private function render_block_fullscreen_script( $container_id ) {
return sprintf(
'<script>
(function() {
var container = document.getElementById("%s");
if (!container) return;

var btn = container.querySelector(".exelearning-fullscreen-btn");
var iframe = container.querySelector(".exelearning-iframe");

if (btn && iframe) {
btn.addEventListener("click", function() {
if (iframe.requestFullscreen) {
iframe.requestFullscreen();
} else if (iframe.webkitRequestFullscreen) {
iframe.webkitRequestFullscreen();
} else if (iframe.msRequestFullscreen) {
iframe.msRequestFullscreen();
}
});
}
})();
</script>',
esc_attr( $container_id )
);
}
}
42 changes: 33 additions & 9 deletions includes/class-export-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,10 @@ private function inject_bootstrap_payload( $template, $attachment_id, $elp_url,
'editorBaseUrl' => $editor_base_url,
);

$bridge_url = EXELEARNING_PLUGIN_URL . 'assets/js/wp-exe-bridge.js?ver=' . EXELEARNING_VERSION;
$bridge_url = EXELEARNING_PLUGIN_URL . 'assets/js/wp-exe-bridge.js';

// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Standalone HTML page output, not a WordPress template; scripts must be inline.
$inject = sprintf(
'<script>
$script = sprintf(
'
window.__WP_EXE_CONFIG__ = %1$s;
window.__EXE_STATIC_MODE__ = true;
window.__EXE_WP_MODE__ = true;
Expand All @@ -203,14 +202,39 @@ private function inject_bootstrap_payload( $template, $attachment_id, $elp_url,
trustedOrigins: [window.location.origin],
hideUI: { fileMenu: true, saveButton: true, userMenu: true },
};
</script>
<script src="%4$s" defer></script>',
',
wp_json_encode( $config ),
wp_json_encode( $editor_base_url ),
wp_json_encode( $elp_url ),
esc_url( $bridge_url )
wp_json_encode( $elp_url )
);
// phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
// Standalone document without a theme header: print only our handle, with
// the config inline before it. The bridge stays deferred.
$handle = 'exelearning-export-bridge';
wp_register_script( $handle, $bridge_url, array(), EXELEARNING_VERSION, false );
wp_enqueue_script( $handle );
wp_script_add_data( $handle, 'strategy', 'defer' );
wp_add_inline_script( $handle, $script, 'before' );

// ponytail: WordPress < 6.3 ignores the loading strategy; drop this
// fallback once "Requires at least" reaches 6.3.
$legacy_defer = static function ( $tag, $tag_handle ) use ( $handle ) {
// $tag also holds the inline config before it; only the tag with a src
// gets the attribute.
if ( $handle !== $tag_handle || preg_match( '/<script\b[^>]*\sdefer\b/', $tag ) ) {
return $tag;
}
return preg_replace( '/<script\b([^>]*\ssrc=)/', '<script defer$1', $tag, 1 );
};
$is_legacy = version_compare( get_bloginfo( 'version' ), '6.3', '<' );
if ( $is_legacy ) {
add_filter( 'script_loader_tag', $legacy_defer, 10, 2 );
}
ob_start();
wp_print_scripts( array( $handle ) );
$inject = ob_get_clean();
if ( $is_legacy ) {
remove_filter( 'script_loader_tag', $legacy_defer, 10 );
}

// Inject config and our bridge before </head>.
$template = str_replace( '</head>', $inject . '</head>', $template );
Expand Down
Loading
Loading