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): ?>
= esc_html($postData['name']) ?>
$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();
?>
= $selector->render($selected, $name . '-selector') ?>
= !empty($field['description']) ? '
' . esc_html($field['description']) . '
' : '' ?>