diff --git a/inc/class-scripts.php b/inc/class-scripts.php index 9d5d980c..5ea5d9c6 100644 --- a/inc/class-scripts.php +++ b/inc/class-scripts.php @@ -37,6 +37,8 @@ public function init(): void { add_action('admin_enqueue_scripts', [$this, 'enqueue_default_admin_scripts']); + add_action('admin_head', [$this, 'print_wubox_early_click_guard'], 1); + add_action('wp_ajax_wu_toggle_container', [$this, 'update_use_container']); add_filter('admin_body_class', [$this, 'add_body_class_container_boxed']); @@ -299,19 +301,7 @@ public function register_default_scripts(): void { */ wp_add_inline_script( 'wubox', - "(function(){ - window.__wuboxEarlyClicks=[]; - window.__wuboxEarlyClickHandler=function(e){ - if(window.__wuboxReady)return; - var t=e.target.closest('.wubox'); - if(!t)return; - e.preventDefault(); - e.stopPropagation(); - t.style.cursor='wait'; - window.__wuboxEarlyClicks.push(t); - }; - document.addEventListener('click',window.__wuboxEarlyClickHandler,true); - })();", + $this->get_wubox_early_click_guard_script(), 'before' ); @@ -470,6 +460,54 @@ public function enqueue_default_admin_scripts(string $hook_suffix): void { wp_enqueue_script('wu-password-toggle'); } + /** + * Prints the early wubox click guard in the document head. + * + * Wubox itself is printed in the footer, so links/buttons rendered earlier in + * the admin page can navigate to their AJAX URL before the footer inline script + * runs. Printing the capture listener in the head closes that race for all + * modal form triggers that use the shared `.wubox` class. + * + * @since 2.6.3 + * @return void + */ + public function print_wubox_early_click_guard(): void { + + if ( ! wu_is_wu_page() || ! wp_script_is('wubox', 'enqueued')) { + return; + } + + printf( + "\n\n", + $this->get_wubox_early_click_guard_script() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static JavaScript, no user input. + ); + } + + /** + * Returns the idempotent early wubox click guard script. + * + * @since 2.6.3 + * @return string + */ + protected function get_wubox_early_click_guard_script(): string { + + return "(function(){ + window.__wuboxEarlyClicks=window.__wuboxEarlyClicks||[]; + if(window.__wuboxEarlyClickHandler)return; + window.__wuboxEarlyClickHandler=function(e){ + if(window.__wuboxReady)return; + var t=e.target&&e.target.closest?e.target.closest('.wubox'):null; + if(!t)return; + e.preventDefault(); + e.stopPropagation(); + if(e.stopImmediatePropagation)e.stopImmediatePropagation(); + t.style.cursor='wait'; + window.__wuboxEarlyClicks.push(t); + }; + document.addEventListener('click',window.__wuboxEarlyClickHandler,true); +})();"; + } + /** * Update the use container setting. * diff --git a/tests/WP_Ultimo/Scripts_Test.php b/tests/WP_Ultimo/Scripts_Test.php index 3063467d..76bed4cf 100644 --- a/tests/WP_Ultimo/Scripts_Test.php +++ b/tests/WP_Ultimo/Scripts_Test.php @@ -252,9 +252,44 @@ public function test_init_registers_hooks(): void { $this->assertNotFalse(has_action('init', [$this->scripts, 'register_default_styles'])); $this->assertNotFalse(has_action('admin_enqueue_scripts', [$this->scripts, 'enqueue_default_admin_styles'])); $this->assertNotFalse(has_action('admin_enqueue_scripts', [$this->scripts, 'enqueue_default_admin_scripts'])); + $this->assertNotFalse(has_action('admin_head', [$this->scripts, 'print_wubox_early_click_guard'])); $this->assertNotFalse(has_filter('admin_body_class', [$this->scripts, 'add_body_class_container_boxed'])); } + /** + * Test the wubox early click guard preserves queued clicks. + */ + public function test_wubox_early_click_guard_is_idempotent_and_preserves_queue(): void { + + $reflection = new \ReflectionClass($this->scripts); + $method = $reflection->getMethod('get_wubox_early_click_guard_script'); + + if (PHP_VERSION_ID < 80100) { + $method->setAccessible(true); + } + + $script = $method->invoke($this->scripts); + + $this->assertStringContainsString('window.__wuboxEarlyClicks=window.__wuboxEarlyClicks||[];', $script); + $this->assertStringContainsString('if(window.__wuboxEarlyClickHandler)return;', $script); + $this->assertStringContainsString("e.target.closest('.wubox')", $script); + $this->assertStringContainsString("document.addEventListener('click',window.__wuboxEarlyClickHandler,true);", $script); + } + + /** + * Test register_default_scripts attaches the same early click guard to wubox. + */ + public function test_register_default_scripts_attaches_wubox_early_click_guard(): void { + + $this->scripts->register_default_scripts(); + + $before = wp_scripts()->get_data('wubox', 'before'); + + $this->assertIsArray($before); + $this->assertNotEmpty($before); + $this->assertStringContainsString('window.__wuboxEarlyClicks=window.__wuboxEarlyClicks||[];', implode("\n", $before)); + } + /** * Test register_default_scripts registers expected handles. */