| @@ -7,167 +7,42 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | class Menu { |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | - * Hook suffix of the Texty admin page | |
| 12 | - * | |
| 13 | - * @since 2.0.2 | |
| 14 | - * | |
| 15 | - * @var string | |
| 11 | + * Initialize | |
| 16 | 12 | */ |
| 17 | - private $hook_suffix = ''; | |
| 18 | - | |
| 19 | - /** | |
| 20 | - * Class constructor | |
| 21 | - * | |
| 22 | - * @return void | |
| 23 | - */ | |
| 24 | 13 | public function __construct() { |
| 25 | 14 | add_action( 'admin_menu', [ $this, 'register_menu' ] ); |
| 26 | - | |
| 27 | - // Capture admin notices so the Texty header renders above them. | |
| 28 | - add_action( 'admin_notices', [ $this, 'inject_before_notices' ], -9999 ); | |
| 29 | - add_action( 'admin_notices', [ $this, 'inject_after_notices' ], PHP_INT_MAX ); | |
| 30 | 15 | } |
| 31 | 16 | |
| 32 | 17 | /** |
| 33 | - * Whether the current admin screen is the Texty page | |
| 18 | + * Register admin menu | |
| 34 | 19 | * |
| 35 | - * @since 2.0.2 | |
| 36 | - * | |
| 37 | - * @return bool | |
| 38 | - */ | |
| 39 | - private function is_texty_admin_page() { | |
| 40 | - if ( empty( $this->hook_suffix ) || ! function_exists( 'get_current_screen' ) ) { | |
| 41 | - return false; | |
| 42 | - } | |
| 43 | - | |
| 44 | - $screen = get_current_screen(); | |
| 45 | - | |
| 46 | - return $screen && $screen->id === $this->hook_suffix; | |
| 47 | - } | |
| 48 | - | |
| 49 | - /** | |
| 50 | - * Open a hidden wrapper before admin notices render | |
| 51 | - * | |
| 52 | - * WordPress core relocates stray `.notice` elements to just after the first | |
| 53 | - * `.wp-header-end` node. Opening the wrapper — and printing the catcher — | |
| 54 | - * before any notice fires collects them all inside a hidden container, so | |
| 55 | - * the React header can sit at the very top of the page. | |
| 56 | - * | |
| 57 | - * @since 2.0.2 | |
| 58 | - * | |
| 59 | 20 | * @return void |
| 60 | 21 | */ |
| 61 | - public function inject_before_notices() { | |
| 62 | - if ( ! $this->is_texty_admin_page() ) { | |
| 63 | - return; | |
| 64 | - } | |
| 65 | - | |
| 66 | - echo '<div class="texty-notice-list-hide" id="texty__notice-list">'; | |
| 67 | - echo '<div class="wp-header-end" id="texty__notice-catcher"></div>'; | |
| 68 | - } | |
| 69 | - | |
| 70 | - /** | |
| 71 | - * Close the hidden notice wrapper opened in inject_before_notices() | |
| 72 | - * | |
| 73 | - * @since 2.0.2 | |
| 74 | - * | |
| 75 | - * @return void | |
| 76 | - */ | |
| 77 | - public function inject_after_notices() { | |
| 78 | - if ( ! $this->is_texty_admin_page() ) { | |
| 79 | - return; | |
| 80 | - } | |
| 81 | - | |
| 82 | - echo '</div>'; | |
| 83 | - } | |
| 84 | - | |
| 85 | - /** | |
| 86 | - * Add Texty admin menu | |
| 87 | - * | |
| 88 | - * @return void | |
| 89 | - */ | |
| 90 | 22 | public function register_menu() { |
| 91 | - global $submenu; | |
| 23 | + $position = apply_filters( 'texty_menu_position', 58 ); | |
| 92 | 24 | |
| 93 | - $capability = apply_filters( 'texty_admin_menu_capability', 'manage_options' ); | |
| 94 | - if ( ! current_user_can( $capability ) ) { | |
| 95 | - return; | |
| 96 | - } | |
| 97 | - | |
| 98 | - $menu_position = apply_filters( 'texty_menu_position', 58 ); | |
| 99 | - $slug = 'texty'; | |
| 100 | - $menu_icon = 'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#a0a5aa" d="M10.52 1c.603 0 1.191.056 1.761.164a5.358 5.358 0 00-1.1 2.754 6.538 6.538 0 00-5.737 10.626l.629.773-.966 1.645h5.414a6.538 6.538 0 006.511-7.138 5.355 5.355 0 002.764-1.075 9.423 9.423 0 01-9.275 11.097H0l2.602-4.314-.013-.02A9.423 9.423 0 0110.521 1zm6.018 0a3.462 3.462 0 110 6.923 3.462 3.462 0 010-6.923z"/></svg>' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 101 | - | |
| 102 | - $dashboard = add_menu_page( | |
| 25 | + $menu = add_menu_page( | |
| 103 | 26 | __( 'Texty', 'texty' ), |
| 104 | 27 | __( 'Texty', 'texty' ), |
| 105 | - $capability, | |
| 106 | - $slug, | |
| 28 | + 'manage_options', | |
| 29 | + 'texty', | |
| 107 | 30 | [ $this, 'render_page' ], |
| 108 | - $menu_icon, | |
| 109 | - $menu_position | |
| 31 | + 'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#a0a5aa" d="M10.52 1c.603 0 1.191.056 1.761.164a5.358 5.358 0 00-1.1 2.754 6.538 6.538 0 00-5.737 10.626l.629.773-.966 1.645h5.414a6.538 6.538 0 006.511-7.138 5.355 5.355 0 002.764-1.075 9.423 9.423 0 01-9.275 11.097H0l2.602-4.314-.013-.02A9.423 9.423 0 0110.521 1zm6.018 0a3.462 3.462 0 110 6.923 3.462 3.462 0 010-6.923z"/></svg>' ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode | |
| 32 | + $position | |
| 110 | 33 | ); |
| 111 | 34 | |
| 112 | - $this->hook_suffix = $dashboard; | |
| 113 | - | |
| 114 | - $submenus = apply_filters( | |
| 115 | - 'texty_admin_menu', | |
| 116 | - [ | |
| 117 | - [ | |
| 118 | - 'path' => 'dashboard', | |
| 119 | - 'title' => __( 'Dashboard', 'texty' ), | |
| 120 | - ], | |
| 121 | - [ | |
| 122 | - 'path' => 'gateway', | |
| 123 | - 'title' => __( 'Gateway', 'texty' ), | |
| 124 | - ], | |
| 125 | - [ | |
| 126 | - 'path' => 'notifications', | |
| 127 | - 'title' => __( 'Notifications', 'texty' ), | |
| 128 | - ], | |
| 129 | - [ | |
| 130 | - 'path' => 'logs', | |
| 131 | - 'title' => __( 'Logs', 'texty' ), | |
| 132 | - ], | |
| 133 | - ], | |
| 134 | - $capability, | |
| 135 | - $slug | |
| 136 | - ); | |
| 137 | - | |
| 138 | - foreach ( $submenus as $item ) { | |
| 139 | - $path = 'admin.php?page=' . $slug . '#/' . $item['path']; | |
| 140 | - if ( filter_var( $item['path'], FILTER_VALIDATE_URL ) ) { | |
| 141 | - $path = $item['path']; | |
| 142 | - } | |
| 143 | - $submenu[ $slug ][] = [ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited | |
| 144 | - $item['title'], | |
| 145 | - $capability, | |
| 146 | - $path, | |
| 147 | - ]; | |
| 148 | - } | |
| 149 | - | |
| 150 | - do_action( 'texty_admin_menu', $capability, $menu_position ); | |
| 151 | - | |
| 152 | - add_action( 'admin_print_scripts-' . $dashboard, [ $this, 'enqueue_scripts' ] ); | |
| 35 | + add_action( 'admin_print_scripts-' . $menu, [ $this, 'enqueue_scripts' ] ); | |
| 153 | 36 | } |
| 154 | 37 | |
| 155 | 38 | /** |
| 156 | 39 | * Render the page |
| 157 | 40 | * |
| 158 | - * The header mounts in its own root so it paints at the very top of the | |
| 159 | - * screen, above the captured admin notices. The notice slot deliberately | |
| 160 | - * stays outside the `.texty-app` Tailwind scope — that scope's | |
| 161 | - * importantized preflight would strip core notice styling. Captured | |
| 162 | - * notices are moved into it on the JS side. | |
| 163 | - * | |
| 164 | 41 | * @return void |
| 165 | 42 | */ |
| 166 | 43 | public function render_page() { |
| 167 | - echo '<div id="texty-header" class="texty-app"></div>'; | |
| 168 | - echo '<div id="texty-notices"></div>'; | |
| 169 | - echo '<div id="texty-app" class="texty-app"></div>'; | |
| 44 | + echo '<div id="texty-app"></div>'; | |
| 170 | 45 | } |
| 171 | 46 | |
| 172 | 47 | /** |
| 173 | 48 | * Enqueue JS and CSS |
| @@ -174,75 +49,36 @@ | ||
| 174 | 49 | * |
| 175 | 50 | * @return void |
| 176 | 51 | */ |
| 177 | 52 | public function enqueue_scripts() { |
| 178 | - $asset_path = TEXTY_DIR . '/dist/index.asset.php'; | |
| 53 | + $assets = [ | |
| 54 | + 'version' => TEXTY_VERSION, | |
| 55 | + 'dependencies' => [ | |
| 56 | + 'wp-api-fetch', | |
| 57 | + 'wp-i18n', | |
| 58 | + ], | |
| 59 | + ]; | |
| 179 | 60 | |
| 180 | - if ( ! file_exists( $asset_path ) ) { | |
| 181 | - error_log( 'Texty: missing build artifact ' . $asset_path . ' — run `npm run build`. The admin app was not enqueued.' ); | |
| 61 | + $url = TEXTY_URL . '/dist'; | |
| 182 | 62 | |
| 183 | - return; | |
| 63 | + // for local development | |
| 64 | + // when webpack "hot module replacement" is enabled, this | |
| 65 | + // constant needs to be turned "true" on "wp-config.php" | |
| 66 | + if ( defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV ) { | |
| 67 | + $url = 'http://localhost:8080'; | |
| 184 | 68 | } |
| 185 | 69 | |
| 186 | - $asset_file = include $asset_path; | |
| 187 | - $deps = isset( $asset_file['dependencies'] ) && is_array( $asset_file['dependencies'] ) ? $asset_file['dependencies'] : []; | |
| 188 | - $version = isset( $asset_file['version'] ) ? $asset_file['version'] : TEXTY_VERSION; | |
| 70 | + // register scripts | |
| 71 | + wp_register_script( 'texty-runtime', $url . '/runtime.js', $assets['dependencies'], $assets['version'], true ); | |
| 72 | + wp_register_script( 'texty-vendors', $url . '/vendors.js', [ 'texty-runtime' ], $assets['version'], true ); | |
| 73 | + wp_register_script( 'texty-admin', $url . '/app.js', [ 'texty-vendors' ], $assets['version'], true ); | |
| 74 | + wp_localize_script( 'texty-admin', 'texty', $this->localize_script() ); | |
| 189 | 75 | |
| 190 | - // The shared bundles (`texty-plugin-ui` → window.texty.pluginUi, | |
| 191 | - // `texty-components` → window.texty.components) are registered globally | |
| 192 | - // by Texty\Assets so add-ons can depend on them on both admin and | |
| 193 | - // storefront. `texty-plugin-ui` is already listed in $deps here, since | |
| 194 | - // the SPA imports @wedevs/plugin-ui. | |
| 195 | - // | |
| 196 | - // WP silently drops a handle whose dependency is unregistered, so an | |
| 197 | - // unbuilt shared entry would blank the admin page with no error. Say so | |
| 198 | - // in the log instead of failing invisibly. | |
| 199 | - foreach ( $deps as $dep ) { | |
| 200 | - if ( strpos( $dep, 'texty-' ) === 0 && ! wp_script_is( $dep, 'registered' ) ) { | |
| 201 | - error_log( 'Texty: shared script handle "' . $dep . '" is not registered — the admin app will not load. Run `npm run build`.' ); | |
| 202 | - } | |
| 203 | - } | |
| 76 | + // register styles | |
| 77 | + wp_register_style( 'texty-vendors-css', $url . '/vendors.css', [ 'wp-components' ], $assets['version'] ); | |
| 78 | + wp_register_style( 'texty-css', $url . '/app.css', [ 'texty-vendors-css' ], $assets['version'] ); | |
| 204 | 79 | |
| 205 | - wp_register_script( | |
| 206 | - 'texty-admin', | |
| 207 | - TEXTY_URL . '/dist/index.js', | |
| 208 | - $deps, | |
| 209 | - $version, | |
| 210 | - true | |
| 211 | - ); | |
| 212 | - | |
| 213 | - // Merged instead of wp_localize_script's `var texty = {…}`: the shared | |
| 214 | - // bundles assign onto the same `window.texty` namespace and print | |
| 215 | - // *before* this script (they are dependencies of it), so a plain | |
| 216 | - // assignment would wipe out window.texty.pluginUi and crash the SPA. | |
| 217 | - $localized = wp_json_encode( $this->localize_script() ); | |
| 218 | - | |
| 219 | - wp_add_inline_script( | |
| 220 | - 'texty-admin', | |
| 221 | - 'window.texty = Object.assign( window.texty || {}, ' . ( $localized ? $localized : '{}' ) . ' );', | |
| 222 | - 'before' | |
| 223 | - ); | |
| 224 | - | |
| 225 | - $vendor_style_deps = [ 'wp-components' ]; | |
| 226 | - | |
| 227 | - if ( wp_style_is( 'texty-plugin-ui', 'registered' ) ) { | |
| 228 | - $vendor_style_deps[] = 'texty-plugin-ui'; | |
| 229 | - } | |
| 230 | - | |
| 231 | - wp_register_style( | |
| 232 | - 'texty-vendor-css', | |
| 233 | - TEXTY_URL . '/dist/style-index.css', | |
| 234 | - $vendor_style_deps, | |
| 235 | - $version | |
| 236 | - ); | |
| 237 | - | |
| 238 | - wp_register_style( | |
| 239 | - 'texty-css', | |
| 240 | - TEXTY_URL . '/dist/index.css', | |
| 241 | - [ 'texty-vendor-css' ], | |
| 242 | - $version | |
| 243 | - ); | |
| 244 | - | |
| 80 | + // enqueue scripts and styles | |
| 245 | 81 | wp_enqueue_script( 'texty-admin' ); |
| 246 | 82 | wp_enqueue_style( 'texty-css' ); |
| 247 | 83 | } |
| 248 | 84 | |
| @@ -251,18 +87,9 @@ | ||
| 251 | 87 | * |
| 252 | 88 | * @return array |
| 253 | 89 | */ |
| 254 | 90 | public function localize_script() { |
| 255 | - $i18n = [ | |
| 256 | - 'asset_url' => trailingslashit( TEXTY_URL ) . 'assets/', | |
| 257 | - 'site_name' => get_bloginfo( 'name' ), | |
| 258 | - 'rest_url' => esc_url_raw( rest_url() ), | |
| 259 | - 'ajax_url' => esc_url_raw( admin_url( 'admin-ajax.php' ) ), | |
| 260 | - 'nonce' => wp_create_nonce( 'wp_rest' ), | |
| 261 | - 'version' => [ | |
| 262 | - 'lite' => TEXTY_VERSION, | |
| 263 | - ], | |
| 264 | - ]; | |
| 91 | + $i18n = []; | |
| 265 | 92 | |
| 266 | 93 | return apply_filters( 'texty_localize_script', $i18n ); |
| 267 | 94 | } |
| 268 | 95 | } |