post_type = $post_type; $this->cache = CacheManager::for(jvbNoBase($post_type), WEEK_IN_SECONDS); $this->config = wp_parse_args($config, [ 'multiple' => true, 'maxSelections' => 0, 'search' => true, 'placeholder' => 'Search posts...', 'noResults' => 'No posts found', 'required' => false, 'shop_id' => null, 'base' => '', 'onClose' => null, 'selected' => [] ]); } /** * Render post selector using the single modal system * * @param array $selected Selected post IDs or post objects * @param string $containerId Optional container ID * @return string HTML for the post selector */ public function render(array $selected = [], string $containerId = ''): string { // Mark that selectors are present for footer output TaxonomySelector::markSelectorsPresent(); // Process selected posts $processedSelected = $this->processSelectedPosts($selected); // Create configuration for the single modal system $modalConfig = [ 'taxonomy' => 'post_selector', // Special identifier for post selectors 'postType' => $this->post_type, 'maxSelections' => $this->config['maxSelections'], 'hierarchical' => false, // Posts are not hierarchical 'search' => $this->config['search'], 'createNew' => false, // Don't allow creating posts through selector 'selected' => $processedSelected, 'feed' => false, 'base' => $this->config['base'], 'onSuccess' => null, 'onClose' => $this->config['onClose'], 'shopId' => $this->config['shop_id'], 'placeholder' => $this->config['placeholder'], 'noResults' => $this->config['noResults'] ]; $configJson = htmlspecialchars(json_encode($modalConfig), ENT_QUOTES, 'UTF-8'); $containerAttr = $containerId ? 'data-container="' . esc_attr($containerId) . '"' : ''; ob_start(); ?>
$postData): ?>
$post) { if (is_numeric($post)) { // Post ID $postObj = get_post($post); if ($postObj) { $processed[$postObj->ID] = [ 'name' => $postObj->post_title, 'path' => $postObj->post_title ]; } } elseif (is_object($post) && isset($post->ID)) { // Post object $processed[$post->ID] = [ 'name' => $post->post_title, 'path' => $post->post_title ]; } elseif (is_array($post) && isset($post['id'])) { // Array with post data $processed[$post['id']] = [ 'name' => $post['title'] ?? $post['name'] ?? 'Unknown', 'path' => $post['title'] ?? $post['name'] ?? 'Unknown' ]; } } return $processed; } /** * REST API endpoint for searching posts * * @param WP_REST_Request $request * @return WP_REST_Response */ public function searchPosts(WP_REST_Request $request): WP_REST_Response { $post_type = $request->get_param('post_type') ?: 'post'; $search = $request->get_param('search') ?: ''; $page = max(1, intval($request->get_param('page')) ?: 1); $per_page = 20; $shop_id = $request->get_param('shop_id'); $args = [ 'post_type' => $post_type, 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => 'title', 'order' => 'ASC', 'post_status' => 'publish' ]; // Add search if provided if (!empty($search)) { $args['s'] = sanitize_text_field($search); } // Add shop exclusion if shop_id is set if (!empty($shop_id)) { // Get existing artists in the shop to exclude them $existing_artists = get_posts([ 'post_type' => 'artist', 'posts_per_page' => -1, 'fields' => 'ids', 'tax_query' => [[ 'taxonomy' => BASE.'shop', 'terms' => $shop_id ]] ]); if (!empty($existing_artists)) { $args['post__not_in'] = $existing_artists; } } // Check cache $cache_key = $this->cache->generateKey($args); $cached_result = $this->cache->get($cache_key); if ($cached_result) { return new WP_REST_Response($cached_result); } // Query posts $query = new WP_Query($args); $posts = $query->posts; $results = []; foreach ($posts as $post) { // Get additional post data $city_terms = wp_get_object_terms($post->ID, BASE.'city'); $city = !empty($city_terms) ? $city_terms[0]->name : ''; $results[] = [ 'id' => $post->ID, 'name' => $post->post_title, 'title' => $post->post_title, 'thumbnail' => get_the_post_thumbnail_url($post->ID, 'thumbnail'), 'city' => $city, 'url' => get_permalink($post->ID), 'hasChildren' => false, // Posts don't have children 'path' => $post->post_title ]; } $result = [ 'terms' => $results, // Use 'terms' to match taxonomy selector format 'pagination' => [ 'has_more' => $query->found_posts > ($page * $per_page), 'total_posts' => $query->found_posts, 'current_page' => $page, 'total_pages' => $query->max_num_pages ] ]; // Cache the result $this->cache->set($cache_key, $result, 300); // 5 minute cache return new WP_REST_Response($result); } /** * Register REST API routes for post selection */ public static function registerRestRoutes(): void { register_rest_route('jvb/v1', '/posts/search', [ 'methods' => 'GET', 'callback' => [new self('post'), 'searchPosts'], 'permission_callback' => function($request) { return JVB()->roles()->checkRole(get_current_user(), $request->get_param('post_type')); }, 'args' => [ 'post_type' => [ 'required' => false, 'type' => 'string', 'default' => 'post' ], 'search' => [ 'required' => false, 'type' => 'string', 'default' => '' ], 'page' => [ 'required' => false, 'type' => 'integer', 'default' => 1 ], 'shop_id' => [ 'required' => false, 'type' => 'integer' ] ] ]); } /** * Render post selector field for MetaForm integration * * @param string $name Field name * @param mixed $value Current value * @param array $field Field configuration * @return string HTML output */ public static function renderField(string $name, mixed $value, array $field): string { $post_type = $field['post_type'] ?? 'post'; $selected = is_array($value) ? $value : [$value]; $config = [ 'multiple' => $field['multiple'] ?? true, 'maxSelections' => $field['limit'] ?? 0, 'search' => true, 'placeholder' => $field['placeholder'] ?? "Search {$post_type}...", 'shop_id' => $field['shop_id'] ?? null, 'onClose' => $field['onClose'] ?? 'updatePostSelection' ]; $selector = new self($post_type, $config); ob_start(); ?>
render($selected, $name . '-selector') ?> ' . esc_html($field['description']) . '

' : '' ?>