<?php
|
namespace JVBase\forms;
|
|
use JVBase\JVBIcons;
|
use JVBase\managers\CacheManager;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Term;
|
use WP_Query;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Base class for taxonomy selector components
|
* Provides foundation for StyleSelector, ThemeSelector, etc.
|
*/
|
class TaxonomySelectorOld
|
{
|
protected string $id;
|
protected string $name;
|
protected string $taxonomyName;
|
protected JVBIcons $icon;
|
protected string $plural;
|
|
protected string $taxonomy;
|
protected string $base;
|
|
protected array $selected = [];
|
protected CacheManager $cache;
|
|
/**
|
* @var array Configuration options
|
*/
|
protected array $config;
|
|
/**
|
* Initialize selector
|
*/
|
public function __construct(string $id, string $taxonomy, array $config = [])
|
{
|
$this->id = sanitize_key($id);
|
$this->taxonomy = jvbCheckBase($taxonomy);
|
$this->name = str_replace(BASE, '', $taxonomy);
|
$this->icon = new JVBIcons();
|
$this->cache = new CacheManager('taxonomy');
|
|
$this->base = $config['base'] ?? '';
|
|
$this->config = wp_parse_args($config, [
|
'name' => $id,
|
'multiple' => true,
|
'max_selections'=> 0,
|
'hierarchical' => false,
|
'search' => true,
|
'createNew' => false,
|
'taxonomy' => $this->taxonomy,
|
'required' => false,
|
'group_by_family'=> false,
|
'placeholder' => '',
|
'show_examples' => false,
|
'show_breadcrumbs'=> true,
|
'expand_siblings'=> true,
|
'show_popularity'=> false,
|
'no_results' => 'Nothing here.',
|
'base' => $this->base,
|
// 'association' => array(),
|
'common' => array(),
|
'types' => '', //for feed block implementation
|
'hidden' => false,
|
'label' => '',
|
'renderTemplates' => true,
|
]);
|
|
|
add_action('wp_footer', [$this, 'outputDialog']);
|
|
|
$tax = get_taxonomy($this->taxonomy);
|
|
$this->plural = JVB_TAXONOMY[$taxonomy]['plural'];
|
$this->taxonomyName = JVB_TAXONOMY[$taxonomy]['singular'];
|
}
|
|
/**
|
* Render selector component
|
* @param array $selected
|
*
|
* @return string
|
*/
|
public function render(array $selected = []):string
|
{
|
$this->selected = $selected;
|
|
$wrapper_classes = $this->getWrapperClasses();
|
|
ob_start();
|
?>
|
<div class="<?= esc_attr($wrapper_classes); ?>"
|
id="<?= esc_attr($this->id); ?>"
|
data-taxonomy="<?= esc_attr($this->name); ?>"
|
data-config='<?= esc_attr(wp_json_encode($this->getFrontendConfig($selected))); ?>'>
|
|
<div class="selector-wrapper">
|
<?php $this->renderSelectedItems($selected); ?>
|
</div>
|
<input type="hidden" name="<?= $this->name ?>">
|
</div>
|
<?php
|
return ob_get_clean();
|
}
|
|
/**
|
* @param array $selected
|
*
|
* @return string
|
*/
|
public function renderFeed(array $selected = []):string
|
{
|
|
$this->selected = $selected;
|
|
$wrapper_classes = $this->getWrapperClasses();
|
$icon = new JVBIcons();
|
ob_start();
|
?>
|
<div class="<?= esc_attr($wrapper_classes); ?> type-filter taxonomy-filter"
|
id="<?= esc_attr($this->id); ?>"
|
data-taxonomy="<?= $this->name ?>"
|
data-for="<?= implode(',', $this->config['types']) ?>"
|
<?= $this->config['hidden'] ?>>
|
|
<button type="button" class="filter-toggle row" title="Filter by <?= $this->config['label'] ?>"
|
aria-expanded="false"
|
aria-controls="filter-dropdown-<?= $this->name ?>">
|
<?= $icon->getIcon($this->name, ['title'=>$this->config['label']]) ?>
|
<?= $this->config['label'] ?>
|
</button>
|
<dialog id="selector-modal filter-dropdown-<?= $this->name ?>"
|
class="filter-dropdown"
|
data-taxonomy="<?= $this->name ?>">
|
<?php $this->renderModalContent(); ?>
|
</dialog>
|
|
<form class="selected-terms" hidden></form>
|
</div>
|
<input type="hidden" name="<?= $this->name ?>">
|
<?php
|
return ob_get_clean();
|
}
|
|
/**
|
* Handle artist search requests
|
*/
|
public function handleArtistSearch(WP_REST_Request $request):WP_REST_Response
|
{
|
$query = sanitize_text_field($request->get_param('query'));
|
$page = (int)$request->get_param('page') ?: 1;
|
$per_page = 10;
|
|
$args = [
|
'post_type' => BASE.'artist',
|
'posts_per_page' => $per_page,
|
'paged' => $page,
|
'orderby' => 'title',
|
'order' => 'ASC',
|
'post_status' => 'publish',
|
's' => $query
|
];
|
|
// If shop_id provided, exclude artists already in shop
|
$shop_id = $request->get_param('shop_id');
|
if ($shop_id) {
|
$existing_artists = get_posts([
|
'post_type' => BASE.'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;
|
}
|
}
|
|
$key = $this->cache->generateKey($args);
|
$cache = $this->cache->get($key);
|
if ($cache) {
|
return new WP_REST_Response($cache);
|
}
|
|
$posts = get_posts($args);
|
$results = [];
|
|
foreach ($posts as $post) {
|
$city_terms = wp_get_object_terms($post->ID, BASE.'city');
|
$city = !empty($city_terms) ? $city_terms[0]->name : '';
|
|
$results[] = [
|
'id' => $post->ID,
|
'title' => $post->post_title,
|
'thumbnail' => get_the_post_thumbnail_url($post->ID, 'thumbnail'),
|
'city' => $city,
|
'url' => get_permalink($post->ID)
|
];
|
}
|
|
$result = [
|
'results' => $results,
|
'hasMore' => count($posts) === $per_page
|
];
|
$this->cache->set($key, $result);
|
return new WP_REST_Response($result);
|
}
|
|
|
/**
|
* Get wrapper CSS classes
|
* @return string
|
*/
|
protected function getWrapperClasses():string
|
{
|
$classes = [
|
'jvb-selector',
|
'selector-' . strtolower($this->name)
|
];
|
|
if ($this->config['multiple']) {
|
$classes[] = 'multiple';
|
}
|
if ($this->config['hierarchical']) {
|
$classes[] = 'hierarchical';
|
}
|
if ($this->config['required']) {
|
$classes[] = 'required';
|
}
|
|
return implode(' ', $classes);
|
}
|
|
/**
|
* Get configuration for frontend JavaScript
|
* @return array
|
*/
|
protected function getFrontendConfig(array $selected = []):array
|
{
|
$out = [
|
// 'name' => $this->config['name'],
|
'multiple' => $this->config['multiple'],
|
'maxSelections' => $this->config['max_selections'],
|
// 'hierarchical' => $this->config['hierarchical'],
|
'search' => $this->config['search'],
|
'createNew' => (bool)$this->config['createNew'],
|
'required' => $this->config['required'],
|
// 'placeholder' => $this->config['placeholder'],
|
'noResults' => $this->config['no_results'],
|
// 'group_by_family' => $this->config['group_by_family'],
|
// 'labels' => $this->getLabels(),
|
'base' => $this->config['base'],
|
'selected' => $this->getSelectedData($selected),
|
// 'values' => $this->getAvailableTerms(),
|
// 'hierarchy' => [],
|
// 'breadcrumbs' => [],
|
// 'association' => $this->config['association']
|
// 'common' => $this->config['common']
|
];
|
if ($this->config['hierarchical']) {
|
$out['hierarchy'] = $this->getTermHierarchy();
|
$out['breadcrumbs'] = $this->getBreadcrumbData();
|
}
|
return $out;
|
}
|
|
|
/**
|
* Get theme hierarchy data
|
* @param int $parent ID of parent
|
*
|
* @return array
|
*/
|
protected function getTermHierarchy(int $parent = 0):array
|
{
|
$terms = get_terms([
|
'taxonomy' => $this->taxonomy,
|
'parent' => $parent,
|
'hide_empty' => false
|
]);
|
|
if (is_wp_error($terms)) {
|
return [];
|
}
|
|
$hierarchy = [];
|
foreach ($terms as $term) {
|
$children = $this->getTermHierarchy($term->term_id);
|
$hierarchy[] = [
|
'id' => $term->term_id,
|
'name' => $term->name,
|
'parent' => $term->parent,
|
'children' => $children
|
];
|
}
|
|
return $hierarchy;
|
}
|
/**
|
* Get theme breadcrumb data
|
* @return array
|
*/
|
protected function getBreadcrumbData():array
|
{
|
$breadcrumbs = [];
|
foreach ($this->selected as $term_id => $title) {
|
$breadcrumbs[$term_id] = $this->getTermAncestors($term_id);
|
}
|
return $breadcrumbs;
|
}
|
|
/**
|
* Get term ancestors with names
|
* @param int $term_id
|
*
|
* @return array
|
*/
|
protected function getTermAncestors(int $term_id):array
|
{
|
$cache = $this->cache->get('term-ancestors-'.$term_id);
|
if ($cache) {
|
return $cache;
|
}
|
$ancestors = get_ancestors($term_id, $this->taxonomy, 'taxonomy');
|
$path = [];
|
foreach (array_reverse($ancestors) as $ancestor_id) {
|
$term = get_term($ancestor_id, $this->taxonomy);
|
if ($term && !is_wp_error($term)) {
|
$path[] = [
|
'id' => $term->term_id,
|
'name' => $term->name
|
];
|
}
|
}
|
$this->cache->set('term-ancestors-'.$term_id, $path);
|
return $path;
|
}
|
|
/**
|
* @return array
|
*/
|
protected function getAvailableTerms():array
|
{
|
$cache = $this->cache->get($this->taxonomy . '-terms');
|
if ($cache) {
|
return $cache;
|
}
|
$terms = get_terms([
|
'taxonomy' => $this->taxonomy,
|
'hide_empty' => false,
|
'fields' => 'id=>name'
|
]);
|
|
$result = is_wp_error($terms) ? [] : $terms;
|
$this->cache->set($this->taxonomy . '-terms', $result);
|
return $result;
|
}
|
|
/**
|
* Get text labels
|
* @return array
|
*/
|
protected function getLabels():array
|
{
|
return [
|
'search' => __('Search...', 'jvb'),
|
'select' => __('Select', 'jvb'),
|
'selected' => __('Selected', 'jvb'),
|
'remove' => __('Remove', 'jvb'),
|
'clear' => __('Clear', 'jvb'),
|
'createNew' => __('Create New', 'jvb'),
|
'loading' => __('Loading...', 'jvb'),
|
'saving' => __('Saving...', 'jvb'),
|
'parent_themes' => __('Parent Themes', 'jvb'),
|
'sub_themes' => __('Sub-themes', 'jvb'),
|
'related_themes' => __('Related Themes', 'jvb'),
|
'popular_combinations' => __('Popular Combinations', 'jvb'),
|
'view_examples' => __('View Examples', 'jvb'),
|
'back_to_parent' => __('Back to Parent', 'jvb')
|
];
|
}
|
|
/**
|
* Render selected items
|
* @return void
|
*/
|
protected function renderSelectedItems(array $selected = []):void
|
{
|
if (empty($selected) && empty($this->selected)) {
|
echo '<div class="selected-items"></div>';
|
return;
|
}
|
|
$selected = empty($selected) ? $this->selected : $selected;
|
|
$out = '<div class="selected-items">';
|
foreach ($selected as $ID) {
|
$term = get_term((int)$ID, $this->taxonomy);
|
|
if (!$term || is_wp_error($term)) {
|
continue;
|
}
|
|
$out .= $this->renderSelectedItem($term);
|
}
|
$out .= '</div>';
|
echo $out;
|
}
|
|
/**
|
* Render single selected item
|
* @param WP_Term $term
|
*
|
* @return string
|
*/
|
protected function renderSelectedItem(WP_Term $term):string
|
{
|
|
return '<div class="selected-item" data-id="'.$term->term_id.'">
|
<span class="item-name">'.$term->name.'</span>
|
<button type="button"
|
class="remove-item"
|
aria-label="Remove" title="Remove '.$term->name.'">×</button>
|
</div>';
|
}
|
|
/**
|
* Render modal content
|
* @return void
|
*/
|
protected function renderModalContent():void
|
{
|
?>
|
<div class="modal-content">
|
<header class="modal-header">
|
<h3><?= esc_html($this->getModalTitle()); ?></h3>
|
</header>
|
<div class="actions row">
|
<button type="button" class="cancel" aria-label="Close">×</button>
|
</div>
|
<div class="selected-items">
|
</div>
|
|
<div class="items-wrap">
|
<?php if (!empty($this->config['common'])) : ?>
|
<details class="favourite-terms" open>
|
<summary class="title row btw">Your Go Tos: </summary>
|
<ul></ul>
|
</details>
|
<?php endif; ?>
|
<p class="pagination-info"></p>
|
<nav class="term-navigation row"><button type="button" class="back-to-parent" hidden><?=$this->icon->getIcon('back')?></button></nav>
|
<ul class="items-container"></ul>
|
<p class="loading"> { <span>loading items</span> } </p>
|
<div class="scroll-sentinel"></div>
|
</div>
|
|
|
|
<?php if ($this->config['search'] || $this->config['createNew']) : ?>
|
<div class="search-wrapper">
|
<div class="search-bar">
|
|
<?php if ($this->config['search']) :
|
echo jvbSearch('Search '.$this->plural.'...');
|
endif; ?>
|
</div>
|
<?php if ($this->config['createNew']) : ?>
|
<details class="create-new-term">
|
<summary class="row btw">Add new <?= $this->taxonomyName ?></summary>
|
<div class="loader"></div>
|
<div class="loading-message create-term" hidden>
|
<span id="typed-text"></span>
|
<span class="cursor">|</span>
|
</div>
|
<div class="create-new-term-section">
|
<?= (jvbSiteVerifiesUsers()) ? '<p class="suggestion-prompt">
|
Not finding what you\'re looking for?<br>
|
Suggest a new <span>'.strtolower($this->taxonomyName).'</span>.</p>' : '' ?>
|
|
|
<div class="form-row name-row">
|
<input type="text"
|
name="term_name"
|
placeholder="Enter new <?=$this->taxonomyName?> name"
|
required>
|
</div>
|
|
<div class="form-row parent-row toggle">
|
<label for="select_parent">Nest new <?= $this->taxonomyName ?> under one of these?</label>
|
<select id="select_parent" name="select_parent">
|
<option value="0"> . . . </option>
|
</select>
|
</div>
|
|
<button type="button" class="submit-term">
|
<?= $this->icon->getIcon('add')?>
|
<span><?= (jvbSiteHasTermApproval()) ? 'Suggest '.$this->taxonomyName : 'Create '.$this->taxonomyName?></span>
|
</button>
|
</div>
|
</details>
|
<?php endif; ?>
|
</div>
|
<?php endif; ?>
|
</div>
|
<?php if (jvbCheck('renderTemplates', $this->config)): ?>
|
<template class="loadingItems">
|
<p>{ <span>loading items</span> }</p>
|
</template>
|
<template class="noResults">
|
<p>{ <span>nothing found</span> }</p>
|
</template>
|
<template class="termListItem">
|
<li>
|
<input type ="<?=($this->config['multiple']) ? 'checkbox' : 'radio'?>">
|
<label>
|
<span class="term-name"></span>
|
</label>
|
</li>
|
</template>
|
<template class="termChildrenToggle">
|
<button type="button" class="toggle-children" aria-expanded="false">
|
<?=$this->icon->getIcon('add')?>
|
</button>
|
</template>
|
<template class="selectedTerm">
|
<div class="selected-item">
|
<span class="item-name"></span>
|
<button type="button" class="remove-item"><?=$this->icon->getIcon('close')?></button>
|
</div>
|
</template>
|
<template class="termBreadcrumb">
|
<button type="button" class="path-level"></button>
|
</template>
|
<?php
|
endif;
|
}
|
|
/**
|
* Get modal title
|
* @return string
|
*/
|
protected function getModalTitle():string
|
{
|
$tax_obj = get_taxonomy($this->taxonomy);
|
return sprintf(
|
__('Select %s', 'jvb'),
|
$tax_obj ? $tax_obj->labels->name : __('Items', 'jvb')
|
);
|
}
|
|
|
|
|
/**
|
* Get popularity label
|
* @param int $count
|
*
|
* @return string
|
*/
|
protected function getPopularityLabel(int $count):string
|
{
|
switch (true) {
|
case $count > 100:
|
return __('Very Popular', 'jvb');
|
case $count > 50:
|
return __('Popular', 'jvb');
|
case $count > 10:
|
return __('Common', 'jvb');
|
default:
|
return __('Unique', 'jvb');
|
}
|
}
|
/**
|
* Render single selectable item
|
* @param WP_Term $term
|
* @param bool $selected
|
*
|
* @return void
|
*/
|
protected function renderSelectableItem(WP_Term $term, bool $selected = false):void
|
{
|
?>
|
<input id="<?=$this->base?><?= strtolower($this->taxonomyName).'-'.esc_attr($term->term_id); ?>"
|
type="<?= $this->config['multiple'] ? 'checkbox' : 'radio'; ?>"
|
name="<?=$this->base?><?=strtolower($this->taxonomyName)?><?= esc_attr($this->id); ?>"
|
value="<?= esc_attr($term->term_id); ?>"
|
<?php checked($selected); ?>>
|
<label class="selectable-item" for="<?=$this->base?><?= strtolower($this->taxonomyName).'-'.esc_attr($term->term_id); ?>">
|
<span><?= esc_html($term->name); ?></span>
|
</label>
|
<?php
|
}
|
/**
|
* Get term metadata
|
*/
|
// protected function get_term_meta($term) {
|
// if (!function_exists('carbon_get_term_meta')) {
|
// return [];
|
// }
|
//
|
// return [
|
// 'examples' => $this->get_term_examples($term->term_id),
|
// 'popular_combinations' => $this->get_term_combinations($term->term_id),
|
// 'related_themes' => carbon_get_term_meta($term->term_id, 'jvb_related_themes') ?: []
|
// ];
|
// }
|
|
/**
|
* Get theme examples
|
*/
|
// protected function get_term_examples($term_id, $limit = 4) {
|
// $examples = [];
|
// $posts = get_posts([
|
// 'post_type' => "e_{$this->context}",
|
// 'posts_per_page' => $limit,
|
// 'tax_query' => [[
|
// 'taxonomy' => $this->taxonomy,
|
// 'terms' => $term_id
|
// ]],
|
// 'orderby' => 'rand'
|
// ]);
|
//
|
// foreach ($posts as $post) {
|
// if (has_post_thumbnail($post)) {
|
// $examples[] = [
|
// 'title' => get_the_title($post),
|
// 'thumbnail' => get_the_post_thumbnail_url($post, 'thumbnail'),
|
// 'full' => get_the_post_thumbnail_url($post, 'full')
|
// ];
|
// }
|
// }
|
//
|
// return $examples;
|
// }
|
/**
|
* Render hidden inputs for form submission
|
* @return void
|
*/
|
protected function renderHiddenInputs():void
|
{
|
if (empty($this->selected)) {
|
return;
|
}
|
|
foreach ($this->selected as $term_id => $name) {
|
printf(
|
'<input type="hidden" name="%s" value="%s">',
|
esc_attr($this->id),
|
esc_attr($term_id)
|
);
|
}
|
}
|
|
/**
|
* Get selected terms data for frontend
|
* @return array
|
*/
|
protected function getSelectedData(array $selected = []):array
|
{
|
$data = [];
|
$selected = (empty($selected)) ? $this->selected : $selected;
|
foreach ($selected as $ID) {
|
$term = get_term((int) $ID, $this->taxonomy);
|
if (!$term || is_wp_error($term)) {
|
continue;
|
}
|
|
$data[] = [
|
'id' => $term->term_id,
|
'name' => $term->name,
|
'parent' => $term->parent
|
];
|
}
|
return $data;
|
}
|
|
/**
|
* @var array Loading state messages
|
*/
|
protected $loading_quips = [
|
"Making you look good...",
|
"Processing perfection...",
|
"Converting ink to pixels...",
|
"Teaching robots about art..."
|
];
|
|
/**
|
* Get loading state HTML
|
* @return string
|
*/
|
protected function getLoadingState():string
|
{
|
ob_start();
|
?>
|
<div class="loading-overlay">
|
<div class="loading-spinner"></div>
|
<div class="loading-message">
|
<?= esc_html($this->getRandomQuip()); ?>
|
</div>
|
</div>
|
<?php
|
return ob_get_clean();
|
}
|
|
/**
|
* Get random loading message
|
* @return string
|
*/
|
protected function getRandomQuip():string
|
{
|
return $this->loading_quips[array_rand($this->loading_quips)];
|
}
|
|
public function outputDialog() {
|
?>
|
<dialog class="selector-modal">
|
<?php $this->renderModalContent(); ?>
|
</dialog>
|
<?php
|
}
|
}
|