From 3fb4c4e1083b6c9332daeb9d64a4e5b14e2556a8 Mon Sep 17 00:00:00 2001 From: erseco Date: Sat, 26 Sep 2026 11:22:11 +0100 Subject: [PATCH] Run the embedded editor on WordPress' jQuery and jQuery UI Plugin Directory guideline 13 asks plugins to use the libraries WordPress already ships. The editor bundle loads its own jQuery 3.7.1 and jQuery UI 1.14.1; the editor page now replaces those two tags with core's copies: jQuery (without Migrate, which the editor never ran with) and every jQuery UI and effects handle core registers, in dependency order, resolved from a private WP_Scripts so the page queue is untouched. Core's jQuery calls noConflict(), so a one-line shim restores the global $ the editor expects. The bundled copies stay in dist/static/libs/: the editor embeds them in the SCORM, IMS, EPUB and web packages it exports, which must run without WordPress. --- AGENTS.md | 4 +++ admin/views/editor-bootstrap.php | 11 ++++++++ includes/class-exelearning-editor.php | 38 ++++++++++++++++++++++++++ tests/unit/EditorBootstrapPageTest.php | 25 +++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6caf6fc..09948ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,10 @@ Packages are plain WordPress **attachments** — there is no custom post type. `exelearning/` is a gitignored clone of `exelearning/exelearning`, not a submodule; `dist/static/` is its generated build. Do not hand-edit either. `npm run build` is only a reminder, not a build. +Inside WordPress the editor runs on core's jQuery and jQuery UI +(`ExeLearning_Editor::core_jquery_tags()` replaces the bundle's two tags); the +bundled copies stay in `dist/static/libs/` only because exported packages embed +them to run without WordPress. - `make build-editor`: fetch the source and build it (needs Bun). - `make build-editor-if-needed`: build only when `dist/static/.build-commit` differs diff --git a/admin/views/editor-bootstrap.php b/admin/views/editor-bootstrap.php index c98eb6d..ce431ea 100644 --- a/admin/views/editor-bootstrap.php +++ b/admin/views/editor-bootstrap.php @@ -533,6 +533,17 @@ function normalizeEditorAssetUrl(url) { $exelearning_integration_assets .= ob_get_clean(); $exelearning_template = str_replace( '', $exelearning_integration_assets . '', $exelearning_template ); +// Run the editor on WordPress' jQuery and jQuery UI. The bundled copies stay in +// dist/static/ only because the editor embeds them in exported packages. +$exelearning_template = preg_replace_callback( + '#<(script) src="\./libs/jquery/jquery\.min\.js[^"]*">\s*<\1 src="\./libs/jquery-ui/jquery-ui\.min\.js[^"]*">#', + static function () { + return ExeLearning_Editor::core_jquery_tags(); + }, + $exelearning_template, + 1 +); + // Add tag to set the base URL for all relative paths. // This ensures paths like "files/perm/..." resolve to the static editor directory. // The word boundary matters: the editor's own markup contains diff --git a/includes/class-exelearning-editor.php b/includes/class-exelearning-editor.php index b52dece..12615b5 100644 --- a/includes/class-exelearning-editor.php +++ b/includes/class-exelearning-editor.php @@ -215,6 +215,44 @@ public function build_bootstrap_page( $attachment_id ) { return include EXELEARNING_PLUGIN_DIR . 'admin/views/editor-bootstrap.php'; } + /** + * Script tags that load WordPress' own jQuery and jQuery UI. + * + * The editor bundle ships jQuery and jQuery UI because exported packages must + * run without WordPress; inside WordPress the editor uses core's copies + * instead. Every jQuery UI and effects handle core registers is loaded, in + * dependency order, from a private WP_Scripts so the page queue is untouched. + * Core's jQuery calls noConflict(), and the editor expects a global $. + * + * @return string + */ + public static function core_jquery_tags() { + $scripts = new WP_Scripts(); + $handles = array( 'jquery-core' ); + foreach ( array_keys( $scripts->registered ) as $handle ) { + if ( 0 === strpos( $handle, 'jquery-ui-' ) || 0 === strpos( $handle, 'jquery-effects-' ) ) { + $handles[] = $handle; + } + } + $scripts->all_deps( $handles ); + + $tags = ''; + foreach ( $scripts->to_do as $handle ) { + $script = $scripts->registered[ $handle ]; + // Only jQuery itself and jQuery UI: the bundled jQuery never ran with + // Migrate, and core's wp-a11y/i18n dependencies are not used by the editor. + if ( 'jquery-migrate' === $handle || false === strpos( (string) $script->src, '/js/jquery/' ) ) { + continue; + } + $src = preg_match( '#^(https?:)?//#', $script->src ) ? $script->src : $scripts->base_url . $script->src; + $tags .= wp_get_script_tag( array( 'src' => add_query_arg( 'ver', $script->ver ? $script->ver : $scripts->default_version, $src ) ) ); + if ( 'jquery-core' === $handle ) { + $tags .= wp_get_inline_script_tag( 'window.$ = window.jQuery;' ); + } + } + return $tags; + } + /** * Settings screen URL that explains a missing editor bundle. * diff --git a/tests/unit/EditorBootstrapPageTest.php b/tests/unit/EditorBootstrapPageTest.php index aecbeed..2d48802 100644 --- a/tests/unit/EditorBootstrapPageTest.php +++ b/tests/unit/EditorBootstrapPageTest.php @@ -296,6 +296,31 @@ public function test_relative_asset_paths_in_the_bundle_are_rewritten() { $this->assertStringNotContainsString( 'src="./app/main.js"', $html ); } + /** + * The editor runs on WordPress' jQuery and jQuery UI; the bundled copies are + * left for exported packages only (Plugin Directory guideline 13). + */ + public function test_the_editor_uses_core_jquery_instead_of_the_bundled_copies() { + wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); + ExeLearning_Bundle_Fixture::write( + 'index.html', + '' + . "\n " + . '' + ); + + $html = $this->editor->build_bootstrap_page( $this->make_elpx() ); + + $this->assertStringNotContainsString( 'libs/jquery/jquery.min.js', $html ); + $this->assertStringNotContainsString( 'libs/jquery-ui/jquery-ui.min.js', $html ); + $this->assertStringContainsString( includes_url( 'js/jquery/' ), $html ); + $this->assertStringContainsString( includes_url( 'js/jquery/ui/sortable' ), $html ); + $this->assertStringContainsString( 'window.$ = window.jQuery;', $html ); + $this->assertStringNotContainsString( 'jquery-migrate', $html ); + // Core's copies load before the editor code that relies on them. + $this->assertLessThan( strpos( $html, 'main.js' ), strpos( $html, includes_url( 'js/jquery/ui/sortable' ) ) ); + } + /** * The bridge script is loaded from the plugin's own assets, not from the * editor bundle: it is the WordPress half of the protocol.