PluginProbe
Texty – SMS Notification for WordPress, WooCommerce, Dokan and more / trunk
Texty – SMS Notification for WordPress, WooCommerce, Dokan and more vtrunk
2.0.2 trunk 0.2 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 2.0.0 2.0.1
texty / includes / Admin / Menu.php

Menu.php in Texty – SMS Notification for WordPress, WooCommerce, Dokan and more trunk, at includes/Admin/Menu.php

269 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Texty\Admin;
4
5 /**
6 * Menu Class
7 */
8 class Menu {
9
10 /**
11 * Hook suffix of the Texty admin page
12 *
13 * @since 2.0.2
14 *
15 * @var string
16 */
17 private $hook_suffix = '';
18
19 /**
20 * Class constructor
21 *
22 * @return void
23 */
24 public function __construct() {
25 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 }
31
32 /**
33 * Whether the current admin screen is the Texty page
34 *
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 * @return void
60 */
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 public function register_menu() {
91 global $submenu;
92
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(
103 __( 'Texty', 'texty' ),
104 __( 'Texty', 'texty' ),
105 $capability,
106 $slug,
107 [ $this, 'render_page' ],
108 $menu_icon,
109 $menu_position
110 );
111
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' ] );
153 }
154
155 /**
156 * Render the page
157 *
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 * @return void
165 */
166 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>';
170 }
171
172 /**
173 * Enqueue JS and CSS
174 *
175 * @return void
176 */
177 public function enqueue_scripts() {
178 $asset_path = TEXTY_DIR . '/dist/index.asset.php';
179
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.' );
182
183 return;
184 }
185
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;
189
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 }
204
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
245 wp_enqueue_script( 'texty-admin' );
246 wp_enqueue_style( 'texty-css' );
247 }
248
249 /**
250 * Get the localize script
251 *
252 * @return array
253 */
254 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 ];
265
266 return apply_filters( 'texty_localize_script', $i18n );
267 }
268 }
269