| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\managers\UserTermsManager; |
| | | use JVBase\meta\MetaManager; |
| | | use WP_User; |
| | | use JVBase\base\Site; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\CRUDSkeleton; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * WordPress CRUD Manager |
| | | * Configures CRUDSkeleton for WordPress post types |
| | | */ |
| | | class CRUD { |
| | | protected WP_User $user; |
| | | protected int $user_id; |
| | | protected array $config; |
| | | protected CRUDSkeleton $skeleton; |
| | | protected Cache $cache; |
| | | protected string $content; |
| | | protected string $singular; |
| | | protected string $plural; |
| | | protected array $filters; |
| | | protected array $bulkActions; |
| | | protected MetaManager $meta; |
| | | protected array $taxonomies; |
| | | protected array $statuses; |
| | | protected array $fields; |
| | | protected array $sections; |
| | | protected array $stuck; |
| | | protected array $taxonomies = []; |
| | | protected int $user_id; |
| | | protected Registrar $registrar; |
| | | |
| | | protected bool $userCanPublish = false; |
| | | public function __construct(string $content) { |
| | | $this->registrar = Registrar::getInstance($content); |
| | | |
| | | public function __construct(string $content) |
| | | { |
| | | //If we haven't defined this content, bail early |
| | | if (!array_key_exists($content, JVB_CONTENT)) { |
| | | if (!$this->registrar) { |
| | | return; |
| | | } |
| | | $this->user = wp_get_current_user(); |
| | | $this->user_id = $this->user->ID; |
| | | $this->config = JVB_CONTENT[$content]; |
| | | $this->singular = $this->config['singular']; |
| | | $this->plural = $this->config['plural']; |
| | | |
| | | $this->user_id = get_current_user_id(); |
| | | $this->content = $content; |
| | | $this->fields = jvbGetFields($this->content, 'post'); |
| | | $this->sections = jvbGetSections($this->content, 'post'); |
| | | $this->stuck = [ |
| | | 'post_title', |
| | | 'term_name' |
| | | ]; |
| | | $this->cache = Cache::for('crud')->connect('post')->connect('taxonomy'); |
| | | |
| | | $this->init(); |
| | | } |
| | | |
| | | protected function init():void |
| | | { |
| | | $this->initStatuses(); |
| | | $this->initBulkActions(); |
| | | $this->initTaxonomies(); |
| | | $this->initFilters(); |
| | | $this->meta = new MetaManager(null, 'post', $this->content); |
| | | |
| | | $plural = strtolower($this->config['plural']??$this->content.'s'); |
| | | $this->userCanPublish = (jvbUserIsVerified()) ? |
| | | user_can($this->user_id, "publish_{$plural}") : false; |
| | | |
| | | } |
| | | |
| | | protected function initTaxonomies():void |
| | | { |
| | | $this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) { |
| | | return in_array($this->content, $config['for_content']); |
| | | }); |
| | | } |
| | | |
| | | protected function initStatuses():void |
| | | { |
| | | $this->statuses = (array_key_exists('is_calendar', $this->config)) ? |
| | | [ |
| | | 'all' => [ |
| | | 'icon' => 'calendar', |
| | | 'label' => 'Everything', |
| | | ], |
| | | 'future'=> [ |
| | | 'label' => 'Upcoming', |
| | | 'icon' => 'future', |
| | | ], |
| | | 'past' => [ |
| | | 'label' => 'Past', |
| | | 'icon' => 'past', |
| | | ], |
| | | 'repeat'=> [ |
| | | 'label' => 'Recurring', |
| | | 'icon' => 'repeat', |
| | | ], |
| | | 'draft' => [ |
| | | 'icon' => 'hide', |
| | | 'label' => 'Hidden', |
| | | ], |
| | | 'trash' => [ |
| | | 'label' => 'Scrapped', |
| | | 'icon' => 'delete', |
| | | ], |
| | | ] : |
| | | [ |
| | | 'all' => [ |
| | | 'icon' => 'all', |
| | | 'label' => 'Everything', |
| | | ], |
| | | 'publish'=> [ |
| | | 'icon' => 'publish', |
| | | 'label' => 'Live', |
| | | ], |
| | | 'draft' => [ |
| | | 'icon' => 'hide', |
| | | 'label' => 'Hidden', |
| | | ], |
| | | 'trash' => [ |
| | | 'label' => 'Scrapped', |
| | | 'icon' => 'delete', |
| | | ], |
| | | ]; |
| | | } |
| | | |
| | | protected function initBulkActions():void |
| | | { |
| | | $this->bulkActions = [ |
| | | 'edit' => 'Edit', |
| | | 'publish' => 'Show', |
| | | 'draft' => 'Hide', |
| | | // 'copy' => 'Duplicate', |
| | | 'trash' => 'Scrap' |
| | | ]; |
| | | } |
| | | |
| | | protected function initFilters():void |
| | | { |
| | | $this->filters = [ |
| | | 'status' => $this->statuses, |
| | | 'date' => [ |
| | | 'label' => 'Date', |
| | | 'icon' => 'calendar' |
| | | ] |
| | | ]; |
| | | |
| | | foreach ($this->taxonomies as $taxonomy=> $config) { |
| | | $this->filters['taxonomy'][$taxonomy] = [ |
| | | 'label' => $config['singular'], |
| | | 'icon' => $taxonomy |
| | | ]; |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | // Create and configure skeleton |
| | | $this->skeleton = new CRUDSkeleton(); |
| | | $this->configure(); |
| | | } |
| | | |
| | | public function render():void |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <div class="dashboard-page <?= esc_attr($this->content) ?>"> |
| | | <?php |
| | | $this->renderHeader(); |
| | | $this->renderContent(); |
| | | $this->renderModals(); |
| | | $this->renderTemplates(); |
| | | ?> |
| | | </div> |
| | | <?php |
| | | echo ob_get_clean(); |
| | | |
| | | } |
| | | |
| | | protected function renderHeader():void |
| | | { |
| | | ?> |
| | | <h1>Your <?= $this->config['plural'] ?></h1> |
| | | <?php |
| | | if (array_key_exists('page_description', $this->config)) { |
| | | ?> |
| | | <p class="page-description"><?=$this->config['page_description']?></p> |
| | | <?php |
| | | } |
| | | $this->renderHeaderActions(); |
| | | } |
| | | |
| | | protected function renderHeaderActions():void |
| | | { |
| | | $uploadConfig = [ |
| | | 'type' => 'upload', |
| | | 'subtype' => 'image', |
| | | 'mode' => (jvbCheck('single_image', $this->config)) ? 'direct' : 'selection', |
| | | 'create_new' => true, |
| | | 'label' => (array_key_exists('image_title', $this->config)) ? $this->config['image_title'] : 'Upload More '.$this->config['plural'], |
| | | 'content' => $this->content, |
| | | 'singular' => $this->singular, |
| | | 'plural' => $this->plural, |
| | | 'multiple' => true, |
| | | 'destination' => 'post' |
| | | ]; |
| | | if (!jvbCheck('single_image', $this->config)) { |
| | | $uploadConfig['destination'] = 'post_group'; |
| | | } |
| | | $uploadConfig['destination'] = 'post_group'; |
| | | if (!jvbCheck('single_image', $this->config)) { |
| | | $uploadConfig['group_title'] = 'Create '.$this->config['plural']; |
| | | $uploadConfig['group_description'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p> |
| | | <p>You can also select multiple images and click the "Add to Group" button.</p> |
| | | <p>If a '.$this->singular.' has multiple images, you can select the '.jvbIcon('star').' to set an image as the main one.</p> |
| | | <p>Images left ungrouped will become individual '.$this->plural.'</p> |
| | | <p>Once finished, click the \'Save Changes\' button to send to server for processing.</p>'; |
| | | } else { |
| | | $uploadConfig['description'] = 'Each image will become its own '.$this->singular.'.'; |
| | | } |
| | | ?> |
| | | <button type="button" class="create-item row" title="Create New <?= $this->singular?>"><?=jvbIcon('add') ?><span class="screen-reader-text">Create New <?= $this->singular?></span></button> |
| | | <details open class="uploader"> |
| | | <summary class="row btw"><?= $this->config['upload_title'] ?? 'Bulk Upload '.$this->plural?></summary> |
| | | <?php |
| | | $this->meta->render( |
| | | 'form', |
| | | 'new_'.$this->content, |
| | | $uploadConfig |
| | | /** |
| | | * Configure CRUDSkeleton from WordPress config |
| | | */ |
| | | protected function configure(): void { |
| | | // Basic info |
| | | $this->skeleton |
| | | ->content($this->content, $this->registrar->getSingular(), $this->registrar->getPlural()) |
| | | ->title( |
| | | $this->registrar->config('dashboard')->getTitle(), |
| | | $this->registrar->config('dashboard')->getDescription()?? '' |
| | | ); |
| | | ?> |
| | | </details> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderContent():void |
| | | { |
| | | ?> |
| | | <section class="items-list <?=$this->content?> crud" data-content="<?= $this->content ?>"> |
| | | <?php |
| | | $this->renderFilters(); |
| | | $this->renderBulkControls(); |
| | | ?> |
| | | <div class="<?= $this->content ?> item-grid" role="grid"></div> |
| | | <div class="scroll-sentinel" aria-hidden="true"></div> |
| | | </section> |
| | | <?php |
| | | $state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->content); |
| | | // Initialize meta |
| | | $this->skeleton->addSearch(); |
| | | |
| | | echo '<template class="emptyState">'.$state.'</template>'; |
| | | ?> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderEmptyState():string |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <div class="empty-state"> |
| | | <h3><?=jvbIcon($this->content)?>Nothing here<?=jvbIcon($this->content)?></h3> |
| | | <p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p> |
| | | <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('add')?>" button to add one at a time.</small></p> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function renderFilters():void |
| | | { |
| | | ?> |
| | | <div class="all-filters col start"> |
| | | <div class="search row start nowrap"> |
| | | <span class="label">Search:</span> |
| | | <?= jvbSearch() ?> |
| | | </div> |
| | | <div class="controls col start"> |
| | | <?php |
| | | $this->renderViewFilters(); |
| | | $this->renderStatusFilters(); |
| | | $this->renderOrderFilters(); |
| | | ?> |
| | | </div> |
| | | <div class="filters row start"> |
| | | <span class="label">Filters:</span> |
| | | <?php |
| | | $this->renderTaxonomyFilters(); |
| | | $this->renderDateFilters(); |
| | | ?> |
| | | <button type="button" class="clear-filters row" hidden> |
| | | <?= jvbIcon('close', ['title' => 'Clear']); ?> |
| | | Clear All Filters |
| | | </button> |
| | | </div> |
| | | |
| | | <?= $this->renderColumnSelector(); ?> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderOrderFilters():void |
| | | { |
| | | ?> |
| | | <div class="radio-options order row btw w-full"> |
| | | <?php |
| | | $order = [ |
| | | 'orderby' => [ |
| | | 'date' => 'Order by date created', |
| | | 'alphabetical' => 'Order alphabetically' |
| | | ], |
| | | 'order' => [ |
| | | 'asc' => 'In ascending order (Z-A, oldest to newest)', |
| | | 'desc' => 'In descending order (A-Z, newest to oldest)' |
| | | ] |
| | | ]; |
| | | |
| | | foreach ($order as $o => $option) { |
| | | ?> |
| | | <div class="row start"> |
| | | <span class="label"><?= ucfirst($o)?>:</span> |
| | | <?php |
| | | $i = 0; |
| | | foreach ($option as $opt => $label) { |
| | | ?> |
| | | <input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>> |
| | | |
| | | <label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($opt)?></label> |
| | | <?php |
| | | $i++; |
| | | } |
| | | ?> |
| | | </div> |
| | | <?php |
| | | } |
| | | ?> |
| | | </div> |
| | | <?php |
| | | } |
| | | protected function renderStatusFilters():void |
| | | { |
| | | if (empty($this->statuses)) { |
| | | return; |
| | | // Timeline if applicable |
| | | if ($this->registrar && $this->registrar->hasFeature('is_timeline')) { |
| | | $this->skeleton->setTimeline(); |
| | | } |
| | | ?> |
| | | <div class="radio-options status row"> |
| | | <span class="label">Status:</span> |
| | | <?php |
| | | $i = 1; |
| | | foreach ($this->statuses as $status => $config) { |
| | | $checked = ($i == 1) ? ' checked' : ''; |
| | | ?> |
| | | <input type="radio" class="btn" data-filter="status" value="<?=$status?>" name="status" id="<?=$status?>"<?=$checked?>> |
| | | <label for="<?=$status?>"> |
| | | <?= jvbIcon($config['icon']) ?> |
| | | <span><?=$config['label']?><span class="count"></span></span> |
| | | </label> |
| | | <?php |
| | | $i++; |
| | | } |
| | | ?> |
| | | </div> |
| | | <?php |
| | | |
| | | // Fields and sections |
| | | $this->skeleton->setFields($this->registrar->getFields()); |
| | | |
| | | foreach ($this->registrar->getSections() as $config) { |
| | | $this->skeleton->addSection($config['id'], $config); |
| | | } |
| | | |
| | | // Taxonomies |
| | | $this->initTaxonomies(); |
| | | |
| | | // Statuses |
| | | if ($this->registrar->hasFeature('is_calendar')) { |
| | | $this->skeleton->setCalendar(); |
| | | } |
| | | |
| | | if ($this->registrar->getType() === 'post') { |
| | | $this->skeleton->setDefaultStatus(); |
| | | } else { |
| | | $this->skeleton->setStatuses([]); |
| | | } |
| | | |
| | | |
| | | // Views |
| | | $this->skeleton |
| | | ->addViews() |
| | | ->defaultView('grid'); |
| | | $this->skeleton->addItemActions(); |
| | | |
| | | // Filters |
| | | $this->skeleton->addDateFilter(); |
| | | $this->skeleton->addCustomDateRange($this->addDateRanges()); |
| | | if (!empty($this->taxonomies)) { |
| | | $this->skeleton->addTaxonomyFilter($this->taxonomies, 'user'); |
| | | } |
| | | |
| | | // Capabilities |
| | | $this->skeleton->addCapabilities(['view', 'edit', 'create', 'delete']); |
| | | |
| | | $plural = strtolower($this->registrar->getPlural() ?? $this->content . 's'); |
| | | $canPublish = $this->userIsVerified() && user_can($this->user_id, "publish_{$plural}"); |
| | | $this->skeleton->userCanPublish($canPublish); |
| | | |
| | | // Bulk actions |
| | | $this->skeleton->addBulkActions(['edit', 'publish', 'draft', 'trash']); |
| | | |
| | | // Uploader |
| | | if ($this->registrar->getType() === 'post') { |
| | | $this->setupUploader(); |
| | | } |
| | | |
| | | |
| | | // Sticky fields |
| | | $stuck = ['post_title', 'term_name']; |
| | | if ($this->skeleton->get('isTimeline')) { |
| | | $stuck[] = 'post_thumbnail'; |
| | | } |
| | | $this->skeleton->stickFields($stuck); |
| | | |
| | | // Hook for create button |
| | | add_filter('jvbAdditionalActions', [$this, 'createItem']); |
| | | } |
| | | |
| | | protected function renderViewFilters():void |
| | | { |
| | | ?> |
| | | <div class="radio-options view row"> |
| | | <span class="label">View:</span> |
| | | protected function userIsVerified():bool { |
| | | $membership = Site::membership(); |
| | | |
| | | <?php |
| | | $views = [ |
| | | 'grid' => ['icon' => 'grid', 'label' => 'Grid View'], |
| | | 'list' => ['icon' => 'list', 'label' => 'List View'], |
| | | 'table' => ['icon' => 'table', 'label' => 'Table View'] |
| | | ]; |
| | | |
| | | $first = true; |
| | | foreach ($views as $view => $config): |
| | | ?> |
| | | <input type="radio" |
| | | data-view="<?= esc_attr($view) ?>" |
| | | value="<?= esc_attr($view) ?>" |
| | | class="btn" |
| | | name="view" |
| | | id="view-<?= esc_attr($view) ?>" |
| | | <?= $first ? 'checked' : '' ?>> |
| | | <label for="view-<?= esc_attr($view) ?>" |
| | | title="<?= esc_attr($config['label']) ?>"> |
| | | <?= jvbIcon($config['icon']) ?> |
| | | <span class="screen-reader-text"><?= esc_html($config['label']) ?></span> |
| | | </label> |
| | | <?php |
| | | $first = false; |
| | | endforeach; |
| | | ?> |
| | | </div> |
| | | <?php |
| | | return !($membership && $membership->has('member_verified')) || current_user_can('skip_moderation'); |
| | | } |
| | | /** |
| | | * Render column selector for table view |
| | | * Setup uploader configuration |
| | | */ |
| | | protected function renderColumnSelector(): string { |
| | | ob_start(); |
| | | ?> |
| | | <details class="multi-select" title="Select columns" hidden> |
| | | <summary class="row start nowrap"> |
| | | <?= jvbIcon('columns') ?> |
| | | <span class="labels">Toggle Columns</span> |
| | | </summary> |
| | | <div class="column-list"> |
| | | <?php foreach ($this->fields as $fieldName => $config): |
| | | if (array_key_exists('hidden', $config)){ |
| | | continue; |
| | | } |
| | | ?> |
| | | <input type="checkbox" |
| | | id="show-<?= esc_attr($fieldName) ?>" |
| | | class="column-toggle ch" |
| | | name="show-<?= esc_attr($fieldName) ?>" |
| | | checked> |
| | | <label for="show-<?= esc_attr($fieldName) ?>"> |
| | | <?= esc_html($config['label']) ?> |
| | | </label> |
| | | <?php endforeach; ?> |
| | | </div> |
| | | </details> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | protected function renderTaxonomyFilters():void |
| | | { |
| | | if (empty($this->taxonomies)) { |
| | | return; |
| | | } |
| | | $out = ''; |
| | | foreach ($this->taxonomies as $taxonomy => $config) { |
| | | $terms = $this->getCommonTerms($taxonomy); |
| | | if (!empty($terms)) { |
| | | $out .= sprintf( |
| | | '<div class="row nowrap"><label for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label> |
| | | <select id="filter-%s" class="filter %s" name="%s" data-filter="taxonomies" data-taxonomy="%s"> |
| | | <option value="">by %s</option>', |
| | | $taxonomy, |
| | | jvbIcon($config['icon'], ['title' => $config['plural']]), |
| | | esc_html($config['plural']), |
| | | $taxonomy, |
| | | $taxonomy, |
| | | $taxonomy, |
| | | $taxonomy, |
| | | $config['plural'] |
| | | ); |
| | | protected function setupUploader(): void { |
| | | |
| | | $isSingleImage = $this->registrar->hasFeature('single_image'); |
| | | |
| | | foreach ($terms as $term) { |
| | | $out .= sprintf( |
| | | '<option value="%s">%s</option>', |
| | | esc_attr($term['term_id']), |
| | | esc_html($term['name']) |
| | | ); |
| | | } |
| | | $out .= '</select></div>'; |
| | | } |
| | | $config = [ |
| | | 'type' => 'upload', |
| | | 'subtype' => 'image', |
| | | 'mode' => $isSingleImage ? 'direct' : 'selection', |
| | | 'create_new' => true, |
| | | 'label' => $this->registrar->getUploadTitle(), |
| | | 'content' => $this->content, |
| | | 'singular' => $this->registrar->getSingular(), |
| | | 'plural' => $this->registrar->getPlural(), |
| | | 'multiple' => true, |
| | | 'destination' => $isSingleImage ? 'post' : 'post_group' |
| | | ]; |
| | | |
| | | if (!$isSingleImage) { |
| | | $config['upload_text'] = '<p>Drag images into groups. Each group becomes its own ' . $this->registrar->getSingular() . '.</p> |
| | | <p>You can also select multiple images and click the "Add to Group" button.</p> |
| | | <p>If a ' . $this->registrar->getSingular() . ' has multiple images, you can select the ' . jvbDashIcon('star') . ' to set an image as the main one.</p> |
| | | <p>Images left ungrouped will become individual ' . $this->registrar->getPlural() . '</p> |
| | | <p>Once finished, click the \'Save Changes\' button to send to server for processing.</p>'; |
| | | } else { |
| | | $config['description'] = 'Each image will become its own ' . $this->registrar->getSingular() . '.'; |
| | | } |
| | | echo $out; |
| | | |
| | | $this->skeleton->addUploader($config); |
| | | } |
| | | |
| | | /** |
| | | * Get common terms for taxonomy |
| | | * @param string $taxonomy |
| | | * @return array |
| | | * Initialize taxonomies from WordPress config |
| | | */ |
| | | protected function getCommonTerms(string $taxonomy):array { |
| | | $manager = new UserTermsManager(); |
| | | return $manager->getUserTerms($this->user_id, $taxonomy); |
| | | protected function initTaxonomies(): void { |
| | | $this->taxonomies = ($this->registrar->getType() === 'post') ? $this->registrar->registrar->taxonomies : []; |
| | | } |
| | | |
| | | protected function renderDateFilters():void |
| | | /** |
| | | * Add create button to dashboard actions |
| | | */ |
| | | public function createItem(array $actions): array { |
| | | $actions[] = [ |
| | | 'button' => '<button type="button" class="create-item row" title="Create New ' . $this->registrar->getSingular() . '">' |
| | | . jvbDashIcon('plus-square') |
| | | . '<span class="screen-reader-text">Create New ' . $this->registrar->getSingular() . '</span></button>', |
| | | 'content' => '', // Modal is rendered by skeleton |
| | | ]; |
| | | |
| | | return $actions; |
| | | } |
| | | |
| | | protected function addDateRanges():array |
| | | { |
| | | $postType = jvbCheckBase($this->content); |
| | | // Get available months |
| | | global $wpdb; |
| | | $months = $wpdb->get_results(" |
| | | return $this->cache->remember( |
| | | 'dateRanges', |
| | | function() { |
| | | $postType = jvbCheckBase($this->content); |
| | | // Get available months |
| | | global $wpdb; |
| | | $months = $wpdb->get_results(" |
| | | SELECT DISTINCT |
| | | YEAR(post_date) as year, |
| | | MONTH(post_date) as month |
| | |
| | | AND post_author = '{$this->user_id}' |
| | | ORDER BY post_date DESC |
| | | "); |
| | | |
| | | // Quick filters |
| | | $out = '<div class="row nowrap"> |
| | | <label for="filter-date">'. |
| | | jvbIcon('calendar',['title'=>'Date']). |
| | | '<span class="screen-reader-text">by Date</span> |
| | | </label> |
| | | <select id="filter-date" class="date-filter" data-filter="date"> |
| | | <option value="">by Date</option> |
| | | <option value="today">Today</option> |
| | | <option value="week">Past Week</option> |
| | | <option value="month">Past Month</option> |
| | | <option value="year">Past Year</option> |
| | | <option value="custom">Custom Range...</option> |
| | | </select> |
| | | </div>'; |
| | | |
| | | $form = '<div class="custom-range row"> |
| | | <label for="date-start" class="col"> |
| | | From |
| | | </label> |
| | | <input type="date" id="date-start" class="date-start"> |
| | | <label for="date-end" class="col"> |
| | | To |
| | | </label> |
| | | <input type="date" id="date-end" class="date-end"> |
| | | </div> |
| | | <div class="month-picker"> |
| | | <label> |
| | | <span>Or select month</span> |
| | | <select class="month-select"> |
| | | <option value="">  . . .  </option>'; |
| | | |
| | | |
| | | foreach ($months as $date) { |
| | | $month_name = date('F Y', mktime(0, 0, 0, $date->month, 1, $date->year)); |
| | | $value = $date->year . '-' . str_pad($date->month, 2, '0', STR_PAD_LEFT); |
| | | $form .= sprintf( |
| | | '<option value="%s">%s</option>', |
| | | esc_attr($value), |
| | | esc_html($month_name) |
| | | ); |
| | | } |
| | | |
| | | $form .= '</select> |
| | | </label> |
| | | </div>'; |
| | | |
| | | // Custom date range |
| | | $out .= jvbNewModal( |
| | | 'date-range', |
| | | 'Filter Results by Date:', |
| | | $form |
| | | ); |
| | | |
| | | echo $out; |
| | | } |
| | | |
| | | protected function renderBulkControls():void |
| | | { |
| | | if (empty($this->bulkActions)) { |
| | | return; |
| | | } |
| | | ?> |
| | | <div class="bulk-controls row nowrap btw"> |
| | | <div class="bulk-select"> |
| | | <input type="checkbox" id="select-all" class="select-all"> |
| | | <label for="select-all" class="row"><span>Select All</span><span class="selected-count" hidden></span></label> |
| | | </div> |
| | | <div class="bulk-actions row nowrap" hidden> |
| | | <label for="bulk-action-select" class="screen-reader-text"> |
| | | Select what to do with this selection. |
| | | </label> |
| | | <select class="bulk-action-select" id="bulk-action-select"> |
| | | |
| | | </select> |
| | | </div> |
| | | </div> |
| | | |
| | | <template class="notTrashOptions"> |
| | | <select class="wrap"> |
| | | <option value="">Bulk Actions...</option> |
| | | <?php |
| | | foreach ($this->bulkActions as $control => $label) { |
| | | $disabled = ($control === 'publish' && !$this->userCanPublish) ? ' disabled' : ''; |
| | | ?> |
| | | <option value="<?=$control?>"<?=$disabled?>><?=$label?></option> |
| | | <?php |
| | | $ranges = []; |
| | | foreach ($months as $date) { |
| | | $month_name = date('F Y', mktime(0, 0, 0, $date->month, 1, $date->year)); |
| | | $value = $date->year . '-' . str_pad($date->month, 2, '0', STR_PAD_LEFT); |
| | | $ranges[$value] = $month_name; |
| | | } |
| | | foreach ($this->taxonomies as $taxonomy => $config) { |
| | | ?> |
| | | <option value="tax-<?=$taxonomy?>">Add to <?= $config['singular'] ?></option> |
| | | <?php |
| | | } |
| | | ?> |
| | | </select> |
| | | |
| | | </template> |
| | | <template class="trashOptions"> |
| | | <select class="wrap"> |
| | | <option value="">Bulk Actions...</option> |
| | | <option value="restore">Restore</option> |
| | | <option value="delete">Permanently Delete</option> |
| | | </select> |
| | | </template> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderModals():void |
| | | { |
| | | $this->renderCreateModal(); |
| | | $this->renderEditModal(); |
| | | $this->renderBulkEditModal(); |
| | | } |
| | | protected function renderCreateModal():void |
| | | { |
| | | echo jvbNewModal( |
| | | 'create', |
| | | 'Creating <span class="count"></span> New '.$this->config['singular'], |
| | | str_replace('edit-form"', 'create-form" data-noautosave', $this->editForm()) |
| | | ); |
| | | } |
| | | |
| | | protected function editForm():string |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>"> |
| | | <input type="hidden" name="form-id" value="<?=uniqid('new-')?>" /> |
| | | <input type="hidden" name="content" value="<?=$this->content?>" /> |
| | | <div class="fields"> |
| | | <div class="field-group radio-options row"> |
| | | <span>Status:</span> |
| | | <?php |
| | | $this->getApplicableStatuses('edit'); |
| | | ?> |
| | | </div> |
| | | <?php if (!$this->userCanPublish) { ?> |
| | | <p class="description">Your account needs to be verified before you can publish content.</p> |
| | | <?php } |
| | | |
| | | if (!empty($this->sections)) { |
| | | $tabs = []; |
| | | foreach ($this->sections as $slug => $title) { |
| | | $tabs[$slug] = [ |
| | | 'title' => $title, |
| | | 'content' => '', |
| | | 'description' => jvbSectionDescription($slug)??'', |
| | | ]; |
| | | $icon = jvbSectionIcon($slug); |
| | | if ($icon !== '') { |
| | | $tabs[$slug]['icon'] = $icon; |
| | | } |
| | | } |
| | | } else { |
| | | $tabs = false; |
| | | } |
| | | $fields = $this->fields; |
| | | |
| | | $first = ['post_thumbnail', 'post_title', 'price']; |
| | | foreach ($first as $f) { |
| | | if (array_key_exists($f, $fields)) { |
| | | if ($tabs) { |
| | | $tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true); |
| | | } else { |
| | | $this->meta->render('form', $f, $fields[$f]); |
| | | } |
| | | |
| | | unset($fields[$f]); |
| | | } |
| | | } |
| | | foreach ($fields as $n => $config) { |
| | | if ($tabs) { |
| | | $section = (array_key_exists('section', $config)) ? $config['section'] : 'basic'; |
| | | $tabs[$section]['content'] .= $this->meta->render('form', $n, $config, false, true); |
| | | } else { |
| | | $this->meta->render('form', $n, $config); |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | if ($tabs) { |
| | | jvbRenderTabs($tabs); |
| | | } |
| | | ?> |
| | | </div> |
| | | </form> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function getApplicableStatuses(string $prefix) { |
| | | foreach ($this->statuses as $status => $config) { |
| | | if ($status === 'all') { |
| | | continue; |
| | | return $ranges; |
| | | } |
| | | if (in_array($status, ['future', 'past'])) { |
| | | if ($status === 'future') { |
| | | $status = 'publish'; |
| | | $config = [ |
| | | 'icon' => 'publish', |
| | | 'label' => 'Live', |
| | | ]; |
| | | } else { |
| | | continue; |
| | | } |
| | | } |
| | | $disabled = ($status === 'publish' && !$this->userCanPublish) ? ' disabled' : ''; |
| | | ?> |
| | | <input type ="radio" |
| | | name="post_status" |
| | | class="btn" |
| | | value="<?= esc_attr($status)?>" |
| | | id="<?=$prefix?>set-<?= esc_attr($status) ?>" |
| | | <?= $disabled?>> |
| | | <label for="<?=$prefix?>set-<?=esc_attr($status)?>"> |
| | | <?= jvbIcon($config['icon'], ['title' => $config['label']]) ?> |
| | | <span><?= esc_html($config['label'])?></span> |
| | | </label> |
| | | <?php |
| | | } |
| | | } |
| | | protected function renderEditModal():void |
| | | { |
| | | echo jvbNewModal( |
| | | 'edit', |
| | | 'Edit your '.$this->singular, |
| | | $this->editForm() |
| | | ); |
| | | } |
| | | |
| | | protected function renderBulkEditModal():void |
| | | { |
| | | if (empty($this->bulkActions)) return; |
| | | ob_start(); |
| | | ?> |
| | | <form class="bulk-edit-form" data-save="content" data-form-id="bulk-edit-<?=$this->content?>"> |
| | | <div class="selected"></div> |
| | | <p class="description">You can unselect items by clicking the image here.</p> |
| | | <p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p> |
| | | <div class="fields"> |
| | | <div class="field-group radio-options row"> |
| | | <?php |
| | | $this->getApplicableStatuses('bulk-'); |
| | | ?> |
| | | </div> |
| | | <?php |
| | | if (!empty($this->taxonomies)) { |
| | | ?> |
| | | <div class="taxonomies"> |
| | | <?php |
| | | foreach ($this->taxonomies as $taxonomy => $config) { |
| | | $this->meta->render( |
| | | 'form', |
| | | 'bulk-edit-'.$taxonomy, |
| | | [ |
| | | 'type' => 'taxonomy', |
| | | 'label' => $config['singular'], |
| | | 'taxonomy' => $taxonomy, |
| | | 'createNew' => jvbUserIsVerified(), |
| | | 'multiple' => true, |
| | | 'mode' => 'append' |
| | | ] |
| | | ); |
| | | } |
| | | ?> |
| | | </div> |
| | | <?php |
| | | } |
| | | $fields = $this->fields; |
| | | $fields = array_filter($fields, function ($field) { |
| | | return array_key_exists('bulkEdit', $field); |
| | | }); |
| | | foreach ($fields as $fieldName => $config) { |
| | | $this->meta->render('form', $fieldName, $config); |
| | | } |
| | | ?> |
| | | </div> |
| | | </form> |
| | | <template class="bulkItem"> |
| | | <label> |
| | | <input type="checkbox"> |
| | | <img> |
| | | </label> |
| | | </template> |
| | | <?php |
| | | $form = ob_get_clean(); |
| | | echo jvbNewModal( |
| | | 'bulkEdit', |
| | | 'Bulk Edit <span class="selected"></span> '.$this->config['plural'], |
| | | $form |
| | | ); |
| | | } |
| | | |
| | | protected function renderTemplates():void |
| | | { |
| | | $this->renderListView(); |
| | | $this->renderGridView(); |
| | | $this->renderTableView(); |
| | | $this->renderTableRow(); |
| | | echo jvbGetEmptyStateTemplate(); |
| | | echo jvbGetGalleryPreviewTemplate(); |
| | | |
| | | |
| | | } |
| | | |
| | | protected function renderItemSelect():string |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <div class="item-select"> |
| | | <input type="checkbox" class="select-item"> |
| | | <label class="select-item-label"> |
| | | <span class="screen-reader-text">Select this <?= $this->singular ?></span> |
| | | </label> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function renderImage():string |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <img loading="lazy" alt=""> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function renderItemActions():string |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <div class="item-actions"> |
| | | <button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>"> |
| | | <?=jvbIcon('edit')?> |
| | | <span class="screen-reader-text">Edit <?= $this->singular ?></span> |
| | | </button> |
| | | <button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>"> |
| | | <?=jvbIcon('delete')?> |
| | | <span class="screen-reader-text">Scrap <?= $this->singular ?></span> |
| | | </button> |
| | | <!-- <button type="button" class="action" data-action="toggle-status">--> |
| | | <!-- <span class="screen-reader-text">Toggle --><?php //= $this->singular ?><!-- Visibility</span>--> |
| | | <!-- </button>--> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function renderItemFields(bool $form = false):string |
| | | { |
| | | ob_start(); |
| | | foreach ($this->fields as $name => $config) { |
| | | $renderMode = $form ? 'form' : 'render'; |
| | | |
| | | $field = $this->meta->render($renderMode, $name, $config, false, true); |
| | | |
| | | // Special handling for title in grid view |
| | | if ($name === 'post_title' && !$form) { |
| | | $field = str_replace('<p', '<h3', str_replace('</p>', '</h3>', $field)); |
| | | } |
| | | |
| | | echo $field; |
| | | } |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function renderGridView():void |
| | | { |
| | | ?> |
| | | <template class="gridView"> |
| | | <div class="item <?= $this->content ?>"> |
| | | <input type="checkbox" class="select-item" name="select-item"> |
| | | <label title="Select this <?= $this->singular?>" class="select-item-label"> |
| | | <?= $this->renderImage() ?> |
| | | </label> |
| | | <?= $this->renderItemActions(); ?> |
| | | </div> |
| | | </template> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderListView():void |
| | | { |
| | | ?> |
| | | <template class="listView"> |
| | | <div class="item <?=esc_attr($this->content)?> row nowrap"> |
| | | <?= $this->renderItemSelect()?> |
| | | <?=$this->renderImage() ?> |
| | | <div class="col start w-full"> |
| | | <?= $this->renderItemActions()?> |
| | | <h3 data-field="post_title"></h3> |
| | | <p data-attr="date"></p> |
| | | <p data-field="price"></p> |
| | | <div data-field="post_excerpt"></div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderTableView():void |
| | | { |
| | | ?> |
| | | <template class="contentTable"> |
| | | <form class="table" |
| | | data-save="content" |
| | | data-content="<?= esc_attr($this->content) ?>" |
| | | data-form-id="content-table-<?= esc_attr($this->content) ?>"> |
| | | |
| | | <?= $this->renderTableActions() ?> |
| | | |
| | | <table> |
| | | <thead> |
| | | <?= $this->renderTableHeader() ?> |
| | | </thead> |
| | | <tbody> |
| | | <!-- Rows will be inserted here --> |
| | | </tbody> |
| | | <tfoot> |
| | | <?= $this->renderTableHeader() ?> |
| | | </tfoot> |
| | | </table> |
| | | </form> |
| | | </template> |
| | | <?php |
| | | } |
| | | /** |
| | | * Render table row template |
| | | * Render the interface |
| | | */ |
| | | protected function renderTableRow(): void { |
| | | ?> |
| | | <template class="tableView"> |
| | | <tr class="item"> |
| | | <td class="select"> |
| | | <?= $this->renderItemSelect() ?> |
| | | </td> |
| | | <td class="status"> |
| | | <?= $this->renderStatusRadios() ?> |
| | | </td> |
| | | <?php |
| | | $makeDetails = [ |
| | | 'group', |
| | | 'repeater', |
| | | 'checkbox', |
| | | 'radio' |
| | | ]; |
| | | foreach ($this->fields as $name => $config): |
| | | if (array_key_exists('hidden', $config)){ |
| | | continue; |
| | | } |
| | | $makeThisDetailed = (in_array($config['type'], $makeDetails)); |
| | | ?> |
| | | <td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>> |
| | | <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?> |
| | | <?php $this->meta->render('form', $name, $config); ?> |
| | | <?= $makeThisDetailed ? '</details>' : '' ?> |
| | | </td> |
| | | <?php endforeach; ?> |
| | | </tr> |
| | | </template> |
| | | <?php |
| | | } |
| | | /** |
| | | * Render status radio buttons |
| | | */ |
| | | protected function renderStatusRadios(): string { |
| | | ob_start(); |
| | | ?> |
| | | <div class="radio-options status-options row"> |
| | | <?php foreach ($this->statuses as $status => $config): |
| | | if ($status === 'all') continue; |
| | | |
| | | // Handle special cases |
| | | if ($status === 'future') { |
| | | $status = 'publish'; |
| | | $config = [ |
| | | 'icon' => 'publish', |
| | | 'label' => 'Live' |
| | | ]; |
| | | } elseif ($status === 'past') { |
| | | continue; |
| | | } |
| | | ?> |
| | | <input type="radio" |
| | | name="post_status" |
| | | id="status-<?= esc_attr($status) ?>" |
| | | value="<?= esc_attr($status) ?>"> |
| | | <label for="status-<?= esc_attr($status) ?>"> |
| | | <?= jvbIcon($config['icon']) ?> |
| | | <span class="screen-reader-text"><?= esc_html($config['label']) ?></span> |
| | | </label> |
| | | <?php endforeach; ?> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | /** |
| | | * Render table header |
| | | */ |
| | | protected function renderTableHeader(): string { |
| | | ob_start(); |
| | | |
| | | ?> |
| | | <tr> |
| | | <th scope="col" class="select-header"> |
| | | <input type="checkbox" id="select-all" name="select-all"> |
| | | <label for="select-all">All</label> |
| | | </th> |
| | | <th scope="col" class="status-header">Status</th> |
| | | <?php foreach ($this->fields as $name => $config): |
| | | if (array_key_exists('hidden', $config)){ |
| | | continue; |
| | | } |
| | | ?> |
| | | <th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>> |
| | | <?= esc_html($config['label']) ?> |
| | | </th> |
| | | <?php endforeach; ?> |
| | | </tr> |
| | | <?php |
| | | return ob_get_clean(); |
| | | public function render(): void { |
| | | $this->skeleton->render(); |
| | | } |
| | | |
| | | /** |
| | | * Render table action controls |
| | | * Get the skeleton instance for further customization |
| | | */ |
| | | protected function renderTableActions(): string { |
| | | ob_start(); |
| | | ?> |
| | | <div class="table-actions row btw nowrap"> |
| | | <?= jvbRenderToggleTextField( |
| | | 'vertical', |
| | | 'TAB NAV:', |
| | | '', |
| | | jvbIcon('down'), |
| | | jvbIcon('right') |
| | | ) ?> |
| | | |
| | | <button type="button" class="add-row" title="Add new row"> |
| | | <?= jvbIcon('add') ?> |
| | | <span>Add Row</span> |
| | | </button> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | public function getSkeleton(): CRUDSkeleton { |
| | | return $this->skeleton; |
| | | } |
| | | } |