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.