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[^"]*">\1>\s*<\1 src="\./libs/jquery-ui/jquery-ui\.min\.js[^"]*">\1>#',
+ 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.