hook_suffix = add_menu_page( __( 'SLB Editor', 'slb-section-layout-builder' ), __( 'SLB Editor', 'slb-section-layout-builder' ), 'edit_pages', self::MENU_SLUG, array( $this, 'render_page' ), 'dashicons-layout', 80 ); // Intercept before WordPress outputs admin chrome - render our own full-screen page. add_action( 'load-' . $this->hook_suffix, array( $this, 'intercept_fullscreen' ) ); } /** * Fires before WordPress outputs the admin header for our page. * Renders the full-screen editor and exits - render_page() is never reached. */ public function intercept_fullscreen(): void { $this->render_fullscreen_page(); exit(); } public function register_rest_routes(): void { register_rest_route( self::REST_NAMESPACE, '/save', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_save' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/save-draft', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_save_draft' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/sections', array( 'methods' => 'GET', 'callback' => array( $this, 'rest_get_sections' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/context', array( 'methods' => 'GET', 'callback' => array( $this, 'rest_get_context' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/publish', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_publish' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/preview-token', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_create_preview_token' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/page/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( $this, 'rest_get_page' ), 'permission_callback' => array( $this, 'rest_permission' ), 'args' => array( 'post_id' => array( 'validate_callback' => function( $v ) { return is_numeric( $v ) && $v > 0; }, ), ), ) ); register_rest_route( self::REST_NAMESPACE, '/page-settings', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_save_page_settings' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/save-global', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_save_global_region' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/global-region', array( 'methods' => 'GET', 'callback' => array( $this, 'rest_get_global_region' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); register_rest_route( self::REST_NAMESPACE, '/global-regions', array( 'methods' => 'GET', 'callback' => array( $this, 'rest_get_global_regions' ), 'permission_callback' => array( $this, 'rest_permission' ), ) ); } /** * Merge plugin starter-sections with theme sections. Theme always wins on slug collision. * * @param array $ctx Resolved context from Resolver::resolve(). * @return array slug => schema map */ private function get_merged_schemas( array $ctx ): array { $parser = new SLBUILD_SchemaParser(); $schemas = array(); if ( ! empty( $ctx['starter_sections_dir'] ) ) { $schemas = $parser->scan_dir( $ctx['starter_sections_dir'] ); } // Theme schemas override starter schemas for any matching slug. $theme_schemas = $parser->scan_dir( $ctx['sections_dir'] ); $schemas = array_merge( $schemas, $theme_schemas ); // Inject Advanced CSS system fields into every schema so the editor shows them. $system_fields = array( array( 'id' => 'slbuild_margin', 'type' => 'spacing_box', 'label' => 'Margin', 'default' => '', 'system' => true ), array( 'id' => 'slbuild_padding', 'type' => 'spacing_box', 'label' => 'Padding', 'default' => '', 'system' => true ), array( 'id' => 'slbuild_z_index', 'type' => 'number', 'label' => 'Z-Index', 'default' => '', 'system' => true ), ); foreach ( $schemas as $slug => $schema ) { $schemas[ $slug ]['settings'] = array_merge( $schema['settings'] ?? array(), $system_fields ); // F20 - resolve preview_image to full URL for the picker. if ( ! empty( $schema['preview_image'] ) ) { $schemas[ $slug ]['preview_url'] = SLBUILD_URL . 'assets/previews/' . sanitize_file_name( $schema['preview_image'] ); } } return $schemas; } /** * POST /wp-json/slbuild/v1/save-draft * Saves draft without required-field validation - used by live preview debounce. * Body: { post_id, layout, instances } */ public function rest_save_draft( WP_REST_Request $request ): WP_REST_Response { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'error' => 'Invalid nonce' ), 403 ); } $post_id = (int) $request->get_param( 'post_id' ); $layout = $request->get_param( 'layout' ); $instances = $request->get_param( 'instances' ); if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { return new WP_REST_Response( array( 'error' => 'Forbidden' ), 403 ); } if ( ! is_array( $layout ) ) { $layout = array(); } if ( ! is_array( $instances ) ) { $instances = array(); } $store = new SLBUILD_DataStore(); $store->save_draft( $post_id, $layout, $instances ); return new WP_REST_Response( array( 'success' => true ), 200 ); } public function rest_permission(): bool { return current_user_can( 'edit_pages' ); } /** * Validate required fields across all instances in the layout. * Returns an array of human-readable error strings (empty = valid). * * @param int $post_id * @param array $layout * @param array $instances * @return string[] */ private function validate_required_fields( int $post_id, array $layout, array $instances ): array { $ctx = SLBUILD_Resolver::instance()->resolve( $post_id ); $schemas = $this->get_merged_schemas( $ctx ); $errors = array(); foreach ( $layout as $item ) { $type = $item['type'] ?? ''; $instance_id = $item['instance'] ?? ''; $schema = $schemas[ $type ] ?? null; if ( ! $schema ) { continue; } $section_name = $schema['name'] ?? $type; $values = $instances[ $instance_id ]['settings'] ?? array(); foreach ( ( $schema['settings'] ?? array() ) as $field ) { $fid = $field['id'] ?? ''; if ( ! $fid ) { continue; // organizer fields (header/paragraph) have no id } $value = (string) ( $values[ $fid ] ?? '' ); $label = $field['label'] ?? $fid; // Legacy required flag if ( ! empty( $field['required'] ) && '' === $value ) { $errors[] = "{$section_name}: {$label} is required"; continue; // skip further rules if empty required } // F17 - validation rules $rules = $field['validation'] ?? array(); if ( empty( $rules ) ) { continue; } if ( ! empty( $rules['required'] ) && '' === $value ) { $errors[] = "{$section_name}: {$label} is required"; continue; } if ( isset( $rules['min_length'] ) && '' !== $value && strlen( $value ) < (int) $rules['min_length'] ) { $errors[] = "{$section_name}: {$label} must be at least {$rules['min_length']} characters"; } if ( isset( $rules['max_length'] ) && strlen( $value ) > (int) $rules['max_length'] ) { $errors[] = "{$section_name}: {$label} exceeds the {$rules['max_length']}-character limit"; } if ( isset( $rules['min'] ) && '' !== $value && is_numeric( $value ) && (float) $value < (float) $rules['min'] ) { $errors[] = "{$section_name}: {$label} must be at least {$rules['min']}"; } if ( isset( $rules['max'] ) && '' !== $value && is_numeric( $value ) && (float) $value > (float) $rules['max'] ) { $errors[] = "{$section_name}: {$label} cannot exceed {$rules['max']}"; } } } return $errors; } /** * POST /wp-json/slbuild/v1/save * Body: { post_id, layout, instances, _wpnonce } */ public function rest_save( WP_REST_Request $request ): WP_REST_Response { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'error' => 'Invalid nonce' ), 403 ); } $post_id = (int) $request->get_param( 'post_id' ); $layout = $request->get_param( 'layout' ); $instances = $request->get_param( 'instances' ); if ( ! $post_id || ! is_array( $layout ) || ! is_array( $instances ) ) { return new WP_REST_Response( array( 'error' => 'Invalid request data' ), 400 ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new WP_REST_Response( array( 'error' => 'Forbidden' ), 403 ); } $errors = $this->validate_required_fields( $post_id, $layout, (array) $instances ); if ( ! empty( $errors ) ) { return new WP_REST_Response( array( 'error' => implode( '; ', $errors ), 'required_errors' => $errors ), 422 ); } $store = new SLBUILD_DataStore(); $store->save_draft( $post_id, $layout, $instances ); return new WP_REST_Response( array( 'success' => true, 'status' => 'draft' ), 200 ); } /** * POST /wp-json/slbuild/v1/save-global * Body: { global_region, layout, instances } */ public function rest_save_global_region( WP_REST_Request $request ): WP_REST_Response { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'error' => 'Invalid nonce' ), 403 ); } $region = sanitize_key( (string) $request->get_param( 'global_region' ) ); if ( ! $region ) { return new WP_REST_Response( array( 'error' => 'Missing global_region' ), 400 ); } $layout = $request->get_param( 'layout' ); $instances = $request->get_param( 'instances' ); if ( ! is_array( $layout ) ) { $layout = array(); } if ( ! is_array( $instances ) ) { $instances = array(); } $store = new SLBUILD_DataStore(); $store->save_global( $region, $layout, $instances ); return new WP_REST_Response( array( 'success' => true ), 200 ); } /** * GET /wp-json/slbuild/v1/global-region?region=header * Returns the saved layout + instances for a global region. */ public function rest_get_global_region( WP_REST_Request $request ): WP_REST_Response { $region = sanitize_key( (string) $request->get_param( 'region' ) ); if ( ! $region ) { return new WP_REST_Response( array( 'error' => 'Missing region' ), 400 ); } $store = new SLBUILD_DataStore(); $raw = $store->get_global( $region ); $data = is_array( $raw ) ? $raw : array( 'layout' => array(), 'instances' => array() ); if ( empty( $data['instances'] ) || ! is_array( $data['instances'] ) ) { $data['instances'] = new stdClass(); } return new WP_REST_Response( array( 'layout' => $data['layout'] ?? array(), 'instances' => $data['instances'], ), 200 ); } /** * GET /wp-json/slbuild/v1/global-regions * Returns all available global regions (from theme gs_*.php files + any saved regions). */ public function rest_get_global_regions( WP_REST_Request $request ): WP_REST_Response { $ctx = SLBUILD_Resolver::instance()->resolve( null ); $store = new SLBUILD_DataStore(); $seen = array(); $result = array(); // Scan theme sections dir for gs_*.php files. $sections_dir = $ctx['sections_dir'] ?? ''; if ( $sections_dir && is_dir( $sections_dir ) ) { $parser = new SLBUILD_SchemaParser(); foreach ( glob( $sections_dir . '/gs_*.php' ) ?: array() as $file ) { $slug = preg_replace( '/^gs_/', '', basename( $file, '.php' ) ); if ( isset( $seen[ $slug ] ) ) { continue; } $seen[ $slug ] = true; $schema = $parser->parse_file( $file ); $result[] = array( 'slug' => $slug, 'label' => ( $schema && isset( $schema['name'] ) ) ? $schema['name'] : ucwords( str_replace( '-', ' ', $slug ) ), ); } } // Also include any regions with saved data but no gs_ file. foreach ( $store->get_all_global_region_keys() as $slug ) { if ( isset( $seen[ $slug ] ) ) { continue; } $result[] = array( 'slug' => $slug, 'label' => ucwords( str_replace( '-', ' ', $slug ) ), ); } usort( $result, function ( $a, $b ) { return strcmp( $a['label'], $b['label'] ); } ); return new WP_REST_Response( $result, 200 ); } /** * POST /wp-json/slbuild/v1/page-settings * Body: { post_id, header, footer } */ public function rest_save_page_settings( WP_REST_Request $request ): WP_REST_Response { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'error' => 'Invalid nonce' ), 403 ); } $post_id = (int) $request->get_param( 'post_id' ); if ( ! $post_id ) { return new WP_REST_Response( array( 'error' => 'Missing post_id' ), 400 ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new WP_REST_Response( array( 'error' => 'Forbidden' ), 403 ); } $store = new SLBUILD_DataStore(); $store->save_page_settings( $post_id, array( 'header' => (string) $request->get_param( 'header' ), 'footer' => (string) $request->get_param( 'footer' ), ) ); return new WP_REST_Response( array( 'success' => true ), 200 ); } /** * GET /wp-json/slbuild/v1/sections * Returns all available sections with their schemas. */ public function rest_get_sections( WP_REST_Request $request ): WP_REST_Response { $ctx = SLBUILD_Resolver::instance()->resolve(); $schemas = $this->get_merged_schemas( $ctx ); return new WP_REST_Response( $schemas, 200 ); } /** * GET /wp-json/slbuild/v1/context * Returns WooCommerce products and categories for editor dropdowns. */ public function rest_get_context( WP_REST_Request $request ): WP_REST_Response { return new WP_REST_Response( $this->build_woo_context(), 200 ); } /** * Build the WooCommerce context array for the editor. */ private function build_woo_context(): array { $context = array( 'products' => array(), 'categories' => array(), 'pages' => array() ); $all_pages = get_posts( array( 'post_type' => 'page', 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC', 'fields' => 'ids', ) ); foreach ( $all_pages as $pid ) { $context['pages'][] = array( 'id' => $pid, 'title' => get_the_title( $pid ) ); } if ( ! class_exists( 'WooCommerce' ) ) { return $context; } $products = get_posts( array( 'post_type' => 'product', 'posts_per_page' => 200, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC', 'fields' => 'ids', ) ); foreach ( $products as $id ) { $context['products'][] = array( 'id' => $id, 'name' => get_the_title( $id ) ); } $cats = get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'number' => 200 ) ); if ( ! is_wp_error( $cats ) ) { foreach ( $cats as $cat ) { $context['categories'][] = array( 'id' => $cat->term_id, 'name' => $cat->name ); } } return $context; } /** * GET /wp-json/slbuild/v1/page/{post_id} * Returns the layout and instance values for a page. */ public function rest_get_page( WP_REST_Request $request ): WP_REST_Response { $post_id = (int) $request->get_param( 'post_id' ); if ( ! current_user_can( 'edit_post', $post_id ) ) { return new WP_REST_Response( array( 'error' => 'Forbidden' ), 403 ); } $store = new SLBUILD_DataStore(); $data = $store->get_page_data( $post_id ); return new WP_REST_Response( $data, 200 ); } /** * POST /wp-json/slbuild/v1/publish * Saves the current state to draft then promotes draft to live. * Body: { post_id, layout, instances } */ public function rest_publish( WP_REST_Request $request ): WP_REST_Response { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'error' => 'Invalid nonce' ), 403 ); } $post_id = (int) $request->get_param( 'post_id' ); $layout = $request->get_param( 'layout' ); $instances = $request->get_param( 'instances' ); if ( ! $post_id || ! is_array( $layout ) || ! is_array( $instances ) ) { return new WP_REST_Response( array( 'error' => 'Invalid request data' ), 400 ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new WP_REST_Response( array( 'error' => 'Forbidden' ), 403 ); } $errors = $this->validate_required_fields( $post_id, $layout, (array) $instances ); if ( ! empty( $errors ) ) { return new WP_REST_Response( array( 'error' => implode( '; ', $errors ), 'required_errors' => $errors ), 422 ); } $store = new SLBUILD_DataStore(); $store->save_draft( $post_id, $layout, $instances ); $store->publish( $post_id ); return new WP_REST_Response( array( 'success' => true, 'status' => 'published' ), 200 ); } /** * POST /wp-json/slbuild/v1/preview-token * Generates a 7-day shareable preview token for logged-out client review. * Body: { post_id } */ public function rest_create_preview_token( WP_REST_Request $request ): WP_REST_Response { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'error' => 'Invalid nonce' ), 403 ); } $post_id = (int) $request->get_param( 'post_id' ); if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { return new WP_REST_Response( array( 'error' => 'Forbidden' ), 403 ); } $token = bin2hex( random_bytes( 32 ) ); set_transient( 'slbuild_preview_' . $post_id, $token, 7 * DAY_IN_SECONDS ); $url = add_query_arg( 'slbuild_preview_token', $token, get_permalink( $post_id ) ); return new WP_REST_Response( array( 'url' => $url, 'expires_in' => 7 * DAY_IN_SECONDS, ), 200 ); } /** * Fallback callback for add_menu_page - never reached; intercept_fullscreen() exits first. */ public function render_page(): void { echo '

' . esc_html__( 'SLB Editor requires JavaScript.', 'slb-section-layout-builder' ) . '

'; } /** * Determine the page type for the given post ID. * Returns one of: front_page, shop, cart, checkout, my_account, product, page. */ private function resolve_page_type( int $post_id ): string { if ( ! $post_id ) { return 'page'; } if ( (int) get_option( 'page_on_front' ) === $post_id ) { return 'front_page'; } if ( function_exists( 'wc_get_page_id' ) ) { $wc_map = array( 'shop' => 'shop', 'cart' => 'cart', 'checkout' => 'checkout', 'myaccount' => 'my_account', ); foreach ( $wc_map as $wc_key => $type ) { if ( (int) wc_get_page_id( $wc_key ) === $post_id ) { return $type; } } } if ( get_post_type( $post_id ) === 'product' ) { return 'product'; } return 'page'; } /** * Outputs the full-screen editor HTML page (no WP admin chrome). */ public function render_fullscreen_page(): void { if ( ! current_user_can( 'edit_pages' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'slb-section-layout-builder' ) ); } $post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification $global_region = isset( $_GET['global_region'] ) ? sanitize_key( $_GET['global_region'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification $return_to = isset( $_GET['return_to'] ) ? (int) $_GET['return_to'] : 0; // phpcs:ignore WordPress.Security.NonceVerification $ctx = SLBUILD_Resolver::instance()->resolve( $post_id ?: null ); $schemas = $this->get_merged_schemas( $ctx ); $store = new SLBUILD_DataStore(); if ( $global_region ) { $raw = $store->get_global( $global_region ); $page_data = is_array( $raw ) ? $raw : array( 'layout' => array(), 'instances' => array() ); } elseif ( $post_id ) { $page_data = $store->get_draft_data( $post_id ); } else { $page_data = array( 'layout' => array(), 'instances' => array() ); } $preview_url = add_query_arg( array( 'slbuild_preview' => '1' ), $post_id ? get_permalink( $post_id ) : home_url( '/' ) ); $slbuild_pages = $this->get_slbuild_pages(); // Ensure the page currently being edited always appears in the switcher, // even if it isn't published yet or doesn't have SLB data saved. if ( $post_id ) { $in_list = false; foreach ( $slbuild_pages as $p ) { if ( (int) $p['id'] === $post_id ) { $in_list = true; break; } } if ( ! $in_list ) { $base_url = admin_url( 'admin.php?page=' . self::MENU_SLUG . '&post_id=' . $post_id ); array_unshift( $slbuild_pages, array( 'id' => $post_id, 'title' => get_the_title( $post_id ), 'editUrl' => $base_url, ) ); } } if ( empty( $page_data['instances'] ) || ! is_array( $page_data['instances'] ) ) { $page_data['instances'] = new stdClass(); } $woo_active = class_exists( 'WooCommerce' ); $woo_fields = array( 'product', 'product_list', 'category', 'collection_source' ); $needs_woo = false; foreach ( $schemas as $schema ) { foreach ( $schema['settings'] ?? array() as $field ) { if ( in_array( $field['type'] ?? '', $woo_fields, true ) ) { $needs_woo = true; break 2; } } } wp_enqueue_media(); wp_enqueue_style( 'buttons' ); wp_enqueue_style( 'slbuild-editor', SLBUILD_URL . 'assets/editor.css', array(), SLBUILD_VERSION ); wp_enqueue_script( 'slbuild-editor', SLBUILD_URL . 'assets/editor.js', array(), SLBUILD_VERSION, true ); $store = new SLBUILD_DataStore(); wp_localize_script( 'slbuild-editor', 'SLBuild', array( 'restUrl' => esc_url_raw( rest_url( self::REST_NAMESPACE ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'postId' => $post_id, 'globalRegion' => $global_region, 'returnTo' => $return_to ?: $post_id, 'schemas' => $schemas, 'pageData' => $page_data, 'previewUrl' => esc_url( $preview_url ), 'wooActive' => $woo_active, 'needsWoo' => $needs_woo, 'context' => $this->build_woo_context(), 'slbPages' => $slbuild_pages, 'pageType' => $this->resolve_page_type( $post_id ), 'themeManifest' => SLBUILD_Resolver::instance()->get_theme_manifest(), 'pageSettings' => $post_id ? $store->get_page_settings( $post_id ) : array( 'header' => 'default', 'footer' => 'default' ), 'adminUrl' => admin_url(), 'connectedFields' => SLBUILD_SettingsHooks::get_connected_fields(), 'previewsUrl' => SLBUILD_URL . 'assets/previews/', 'i18n' => array( 'save' => __( 'Save', 'slb-section-layout-builder' ), 'saved' => __( 'Saved', 'slb-section-layout-builder' ), 'saving' => __( 'Saving...', 'slb-section-layout-builder' ), 'addSection' => __( '+ Add Section', 'slb-section-layout-builder' ), 'noSections' => __( 'No sections found in theme.', 'slb-section-layout-builder' ), 'wooTitle' => __( 'WooCommerce Required', 'slb-section-layout-builder' ), 'wooMessage' => __( 'One or more sections on this page require WooCommerce. WooCommerce is not active - those sections will render empty. Continue anyway?', 'slb-section-layout-builder' ), 'wooContinue' => __( 'Continue Anyway', 'slb-section-layout-builder' ), 'wooCancel' => __( 'Cancel', 'slb-section-layout-builder' ), 'publish' => __( 'Publish', 'slb-section-layout-builder' ), 'publishing' => __( 'Publishing-', 'slb-section-layout-builder' ), 'published' => __( 'Published!', 'slb-section-layout-builder' ), 'discard' => __( 'Discard', 'slb-section-layout-builder' ), 'discardConfirm' => __( 'Discard all unsaved changes?', 'slb-section-layout-builder' ), 'autosaved' => __( 'Autosaved', 'slb-section-layout-builder' ), ), ) ); while ( ob_get_level() > 0 ) { ob_end_clean(); } nocache_headers(); ?> > <?php echo $post_id ? esc_html( get_the_title( $post_id ) ) . ' — ' : ''; ?>SLB Editor
←
'page', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_key' => SLBUILD_DataStore::PAGE_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'orderby' => 'title', 'order' => 'ASC', ) ); $result = array(); foreach ( $pages as $page ) { $base_url = admin_url( 'admin.php?page=' . self::MENU_SLUG . '&post_id=' . $page->ID ); $result[] = array( 'id' => $page->ID, 'title' => $page->post_title, 'editUrl' => $base_url, ); } return $result; } /** * Adds "Edit with SLB" to the Pages list row actions (pages with SLB data only). * * @param array $actions * @param WP_Post $post * @return array */ public function add_page_row_action( array $actions, WP_Post $post ): array { if ( $post->post_type !== 'page' || ! current_user_can( 'edit_post', $post->ID ) ) { return $actions; } $data = get_post_meta( $post->ID, SLBUILD_DataStore::PAGE_META_KEY, true ); if ( empty( $data ) ) { return $actions; } $url = admin_url( 'admin.php?page=' . self::MENU_SLUG . '&post_id=' . $post->ID ); $actions['slbuild_edit'] = 'Edit with SLB'; return $actions; } /** * Adds "Edit with SLB" button on the page edit screen (pages with SLB data only). * * @param WP_Post $post */ public function add_edit_form_button( WP_Post $post ): void { if ( $post->post_type !== 'page' || ! current_user_can( 'edit_post', $post->ID ) ) { return; } $data = get_post_meta( $post->ID, SLBUILD_DataStore::PAGE_META_KEY, true ); if ( empty( $data ) ) { return; } $url = admin_url( 'admin.php?page=' . self::MENU_SLUG . '&post_id=' . $post->ID ); ?>
hook_suffix ) { return; } if ( ! current_user_can( 'edit_pages' ) ) { return; } wp_enqueue_style( 'slbuild-admin-page-flow', SLBUILD_URL . 'assets/admin-page-flow.css', array(), SLBUILD_VERSION ); wp_enqueue_script( 'slbuild-admin-page-flow', SLBUILD_URL . 'assets/admin-page-flow.js', array(), SLBUILD_VERSION, true ); wp_localize_script( 'slbuild-admin-page-flow', 'SLBUILD_PageFlow', array( 'wpRestUrl' => esc_url_raw( rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'editorBase' => esc_url_raw( admin_url( 'admin.php?page=' . self::MENU_SLUG ) ), ) ); } /** * Enqueued inside the Gutenberg block editor for pages only. * Injects the "Edit with SLB" button into the editor top bar. */ public function enqueue_gutenberg_assets(): void { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || $screen->post_type !== 'page' ) { return; } if ( ! current_user_can( 'edit_pages' ) ) { return; } wp_enqueue_style( 'slbuild-admin-page-flow', SLBUILD_URL . 'assets/admin-page-flow.css', array(), SLBUILD_VERSION ); wp_enqueue_script( 'slbuild-gutenberg-btn', SLBUILD_URL . 'assets/gutenberg-slb-button.js', array( 'wp-dom-ready', 'wp-data' ), SLBUILD_VERSION, true ); wp_localize_script( 'slbuild-gutenberg-btn', 'SLBUILD_GutenbergData', array( 'editorBase' => esc_url_raw( admin_url( 'admin.php?page=' . self::MENU_SLUG ) ), ) ); } /** * Adds "Edit with SLB" to the WP admin bar when viewing an SLB page on the front end. * * @param WP_Admin_Bar $wp_admin_bar */ public function add_admin_bar_link( WP_Admin_Bar $wp_admin_bar ): void { if ( ! is_singular() || ! current_user_can( 'edit_pages' ) || isset( $_GET['slbuild_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } $post_id = get_the_ID(); $data = get_post_meta( $post_id, SLBUILD_DataStore::PAGE_META_KEY, true ); if ( empty( $data ) ) { return; } $url = admin_url( 'admin.php?page=' . self::MENU_SLUG . '&post_id=' . $post_id ); $wp_admin_bar->add_node( array( 'id' => 'slb-edit', 'title' => 'Edit with SLB', 'href' => $url, 'meta' => array( 'target' => '_blank' ), ) ); } }