<?php
|
namespace JVBase\forms;
|
|
use JVBase\managers\Cache;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Query;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Post Selector - Extends the single modal taxonomy selector for post selection
|
* Uses the new single modal system for consistent UX
|
*/
|
class PostSelector
|
{
|
protected string $post_type;
|
protected array $config;
|
protected Cache $cache;
|
|
public function __construct(string $post_type, array $config = [])
|
{
|
$this->post_type = $post_type;
|
$this->cache = Cache::for(jvbNoBase($post_type), WEEK_IN_SECONDS)->connect('post', true);
|
|
$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
|
{
|
// 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();
|
?>
|
<div class="post-selector-container" id="<?= $containerId ?: 'post-selector-' . $this->post_type ?>">
|
<button type="button"
|
class="filter-toggle row post-toggle"
|
data-taxonomy-config='<?= $configJson ?>'
|
<?= $containerAttr ?>
|
aria-label="Select <?= esc_attr($this->post_type) ?>">
|
<span class="button-text">Select <?= esc_html(ucfirst($this->post_type)) ?></span>
|
<span class="icon">+</span>
|
</button>
|
|
<div class="selected-items row" role="region" aria-label="Selected <?= esc_attr($this->post_type) ?>">
|
<?php if (!empty($processedSelected)): ?>
|
<?php foreach ($processedSelected as $postId => $postData): ?>
|
<div class="selected-item row" data-id="<?= esc_attr($postId) ?>">
|
<span><?= esc_html($postData['name']) ?></span>
|
<button type="button" class="remove-item row" aria-label="Remove post">×</button>
|
</div>
|
<?php endforeach; ?>
|
<?php endif; ?>
|
</div>
|
</div>
|
<?php
|
return ob_get_clean();
|
}
|
|
/**
|
* Process selected posts into the format expected by single modal
|
*
|
* @param array $selected Array of post IDs or post objects
|
* @return array Processed selected posts
|
*/
|
protected function processSelectedPosts(array $selected): array
|
{
|
$processed = [];
|
|
foreach ($selected as $key => $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();
|
?>
|
<div class="field post-selector">
|
<div class="field-group-header">
|
<label class="toggle">
|
<?= $field['icon'] ?? '' ?>
|
<?= esc_html($field['label'] ?? ucfirst($post_type)) ?>
|
</label>
|
<button title="Add <?= esc_attr(ucfirst($post_type)) ?>"
|
class="add-item-btn"
|
type="button">
|
+
|
</button>
|
</div>
|
|
<?= $selector->render($selected, $name . '-selector') ?>
|
|
<!-- Hidden input for form submission -->
|
<input type="hidden"
|
name="<?= esc_attr($name) ?>"
|
class="post-selector-input"
|
data-post-type="<?= esc_attr($post_type) ?>"
|
value="<?= esc_attr(is_array($value) ? implode(',', $value) : $value) ?>">
|
|
<?= !empty($field['description']) ? '<p class="description">' . esc_html($field['description']) . '</p>' : '' ?>
|
</div>
|
<?php
|
return ob_get_clean();
|
}
|
}
|
|
// Register REST routes
|
add_action('rest_api_init', [PostSelector::class, 'registerRestRoutes']);
|