adminJs(); $this->adminCss(); }); add_filter('admin_body_class', [self::class, 'bodyClass']); } /** * The screens of the editor (Add new and edit). Their ids depend on the translated menu title, so WordPress * builds them. * * @return string[] */ public static function editorScreenIds(): array { return [ get_plugin_page_hookname(SubpageCreate::SLUG, SubpageMaster::SLUG), 'admin_page_' . SubpageEdit::SLUG, ]; } /** * Mark the screens of the editor with a class of our own, so the CSS does not depend on a screen id (that * changes with the language of the menu title). * * @param string $classes The classes WordPress gives the body of an admin screen. */ public static function bodyClass($classes): string { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen && in_array($screen->id, self::editorScreenIds(), true)) { return $classes . ' aop-editor-screen'; } return (string) $classes; } /** * Handles all the admin js files. */ private function adminJs() { Data::adminPagesIds()->filter(function ($page) { return get_current_screen()->id === $page; })->each(function () { // window.aop.fields: the place where the built-in and add-on field types register (Field Types API). // The editor reads it, so it comes first. static::buildScript(Plugin::PREFIX . 'field-registry-js', 'field-registry'); // The handle stays 'aop-app-js': the notices and the localized data address it. static::buildScript(Plugin::PREFIX . 'app-js', 'editor', [Plugin::PREFIX . 'field-registry-js']); static::scriptTranslations(Plugin::PREFIX . 'app-js'); wp_localize_script(Plugin::PREFIX . 'app-js', Plugin::PREFIX_ . 'editor_config', static::editorConfig()); // The checkboxes of the roles are CheckboxControl; its look comes from the stylesheet of WordPress. wp_enqueue_style('wp-components'); wp_localize_script(Plugin::PREFIX . 'app-js', Plugin::PREFIX_ . 'script_data_js', Data::js()->toArray()); wp_localize_script(Plugin::PREFIX . 'app-js', Plugin::PREFIX_ . 'dashicons_js', Dashicons::allIconsCollection()->toArray()); /** * Enqueue the editor scripts of your field types here. Runs on the screens of the editor after the * scripts of the plugin were enqueued. * * @since 1.0.0 */ do_action('aop_enqueue_editor_assets'); }); } /** * What the React editor needs besides the page data: the fields of the form that WordPress always posted * with a page. They travel in a script, not in an attribute named nonce (analysis S14). * * @return array{nonceName: string, nonce: string, referer: string, svgUploads: bool, roles: array, legacyEditorRoles: string[]} */ public static function editorConfig(): array { list($nonceName, $nonce, $referer) = SubpageEdit::wpNonceFieldArray(); return [ 'nonceName' => $nonceName, 'nonce' => $nonce, 'referer' => $referer, // WordPress blocks SVG uploads by default; the image field says so (a plugin like Safe SVG allows them). 'svgUploads' => in_array('image/svg+xml', get_allowed_mime_types(), true), 'roles' => static::roles(), // A page of 1.0 with level "Editor" shows these roles ticked (Access::legacyEditorRoles()). 'legacyEditorRoles' => Access::legacyEditorRoles(), ]; } /** * Every role of the site, also roles that a plugin or the site builder added. Not get_editable_roles(): plugins * hide roles there. Which role may open a page is decided by the role a user has, not by its capabilities; a * role that cannot open the dashboard at all gets a remark in the editor. * * @return array */ public static function roles(): array { $counts = function_exists('wp_is_large_user_count') && wp_is_large_user_count() ? null : count_users()['avail_roles']; $roles = []; foreach (wp_roles()->roles as $slug => $role) { $slug = (string) $slug; $object = get_role($slug); $roles[] = [ 'slug' => $slug, 'name' => translate_user_role($role['name']), 'count' => $counts === null ? null : (int) ($counts[$slug] ?? 0), 'canOpenAdmin' => $object !== null && static::roleCanOpenAdmin($object), ]; } return $roles; } /** * Whether users with this role can open wp-admin. WooCommerce sends users away who have none of edit_posts, * manage_woocommerce and view_admin_dashboard (woocommerce_prevent_admin_access). */ private static function roleCanOpenAdmin(\WP_Role $role): bool { if (!$role->has_cap('read')) { return false; } if (!class_exists('WooCommerce')) { return true; } return $role->has_cap('edit_posts') || $role->has_cap('manage_woocommerce') || $role->has_cap('view_admin_dashboard'); } /** * Handles all the admin css files. */ private function adminCss() { Data::adminPagesIds()->filter(function ($page) { return get_current_screen()->id === $page && get_current_screen()->id !== 'toplevel_page_admin_options_pages_master'; })->each(function () { static::buildStyle(Plugin::PREFIX . 'app-css', 'css/aop'); }); // The list of pages has its own WordPress table; only the dialog and the row link need styles. Data::adminPagesIds()->filter(function ($page) { return get_current_screen()->id === $page && get_current_screen()->id === 'toplevel_page_admin_options_pages_master'; })->each(function () { static::buildStyle(Plugin::PREFIX . 'master-css', 'css/master'); }); wp_enqueue_style(Plugin::PREFIX . 'icon-css', Plugin::assetsUrl() . 'css/aop-icon.css', [], Plugin::releaseVersion(), 'all'); } public static function version($file) { $path = Plugin::assetsDir() . $file; return file_exists($path) ? md5(filemtime($path)) : Plugin::releaseVersion(); } /** * Dependencies and version of a wp-scripts entry, read from its generated .asset.php. * * @param string $entry Entry name, e.g. 'settings'. * * @return array{dependencies: string[], version: string} */ public static function buildAsset($entry) { $file = Plugin::buildDir() . $entry . '.asset.php'; if (is_readable($file)) { $asset = include $file; return [ 'dependencies' => (array) ($asset['dependencies'] ?? []), 'version' => (string) ($asset['version'] ?? Plugin::releaseVersion()), ]; } return ['dependencies' => [], 'version' => static::buildFileVersion($entry . '.js')]; } /** * Version for a built file that has no .asset.php (the CSS files). * * @param string $file Path relative to includes/build/. * * @return string */ public static function buildFileVersion($file) { $path = Plugin::buildDir() . $file; return file_exists($path) ? (string) filemtime($path) : Plugin::releaseVersion(); } /** * Enqueue a wp-scripts JavaScript entry with the dependencies webpack found. * * @param string $handle * @param string $entry Entry name, e.g. 'settings'. * @param string[] $extraDependencies Script handles webpack cannot detect (e.g. 'wp-api'). */ public static function buildScript($handle, $entry, array $extraDependencies = []) { $asset = static::buildAsset($entry); wp_enqueue_script( $handle, Plugin::buildUrl() . $entry . '.js', array_values(array_unique(array_merge($asset['dependencies'], $extraDependencies))), $asset['version'], true ); } /** * Load the translations of a script: a language pack of translate.wordpress.org, otherwise the .json files * that ship with the plugin. * * @param string $handle */ public static function scriptTranslations($handle): void { wp_set_script_translations($handle, 'admin-options-pages', Plugin::languagesDir()); } /** * Enqueue a wp-scripts CSS entry. * * @param string $handle * @param string $entry Entry name, e.g. 'css/settings'. * @param string[] $dependencies Style handles. */ public static function buildStyle($handle, $entry, array $dependencies = []) { wp_enqueue_style($handle, Plugin::buildUrl() . $entry . '.css', $dependencies, static::buildFileVersion($entry . '.css')); } }