Jake Vanderwerf
2026-07-10 f94860aacd6200fb24c9e7431eb379a368cb392d
inc/blocks/FeedBlock.php
@@ -155,18 +155,35 @@
      $args['post_type'] = array_map('jvbCheckBase', $attrs['contentTypes']);
      $this->args = $args;
   }
      protected function getContent():string
      {
         return implode(',', $this->content);
      }
      protected function determineTaxonomies():void
      {
         $taxonomies = [];
         $ignore = [];
         foreach ($this->content as $content) {
   protected function getContent():string
   {
      return implode(',', $this->content);
   }
   protected function determineTaxonomies():void
   {
      $taxonomies = [];
      $ignore = [];
      foreach ($this->content as $content) {
            $registrar = Registrar::getInstance($content);
            if (!$registrar) continue;
         $registrar = Registrar::getInstance($content);
         if (!$registrar) continue;
         if ($registrar->hasFeature('is_content')) {
            foreach ($registrar->registrar->for as $c) {
               $contentRegistrar = Registrar::getInstance($c);
               if (!$contentRegistrar) continue;
               $theTax = $contentRegistrar->registrar->taxonomies;
               foreach ($theTax as $tax) {
                  if (!in_array($tax, $ignore) && !in_array($tax, $taxonomies)) {
                     $taxReg = Registrar::getInstance($tax);
                     if ($taxReg->hasFeature('show_feed')) {
                        $taxonomies[] = $tax;
                     } else {
                        $ignore[] = $tax;
                     }
                  }
               }
            }
         } else {
            $theTax = $registrar->registrar->taxonomies;
            foreach ($theTax as $tax) {
               if (!in_array($tax, $ignore) && !in_array($tax, $taxonomies)) {
@@ -179,8 +196,9 @@
               }
            }
         }
         $this->taxonomies = array_unique($taxonomies);
      }
      $this->taxonomies = array_unique($taxonomies);
   }
   protected function renderFiltersAndControls():string
   {
@@ -199,42 +217,42 @@
         jvbIcon('x')
      );
   }
      protected function renderSearch():string
      {
         return sprintf(
            '<div class="search row left nowrap">
   protected function renderSearch():string
   {
      return sprintf(
         '<div class="search row left nowrap">
            <span class="label">Search:</span>
            %s
            </div>',
            jvbSearch()
         );
      }
      protected function renderContentLabels():string
      {
         return sprintf(
            '<span class="label">Showing: <span class="current">%s</span></span>',
            $this->content[0]??''
         );
         jvbSearch()
      );
   }
   protected function renderContentLabels():string
   {
      return sprintf(
         '<span class="label">Showing: <span class="current">%s</span></span>',
         $this->content[0]??''
      );
      }
      protected function renderContent():string
      {
         $favourites = '';
         if (Site::has('favourites')) {
            $favourites = sprintf(
               '<input type="checkbox" id="favourites" class="btn" name="favourites" value="on" data-filter="favourites">
   }
   protected function renderContent():string
   {
      $favourites = '';
      if (Site::has('favourites')) {
         $favourites = sprintf(
            '<input type="checkbox" id="favourites" class="btn" name="favourites" value="on" data-filter="favourites">
               <label for="favourites" title="Show Favourites">%s%s<span class="screen-reader-text">Show Favourites Only</span></label>',
               jvbIcon('heart'),
               jvbIcon('heart', ['style' => 'fill'])
            );
         }
         if (count($this->content) === 1) {
            return empty($favourites)
               ? sprintf(
                  '<input type="hidden" data-filter="content" name="content" value="%s">',
                  implode(',', $this->content)
               )
               : sprintf(
            jvbIcon('heart'),
            jvbIcon('heart', ['style' => 'fill'])
         );
      }
      if (count($this->content) === 1) {
         return empty($favourites)
            ? sprintf(
               '<input type="hidden" data-filter="content" name="content" value="%s">',
               implode(',', $this->content)
            )
            : sprintf(
               '<div class="content row right">
                  <input type="hidden" name="content" value="%s">
                  %s
@@ -242,42 +260,42 @@
               implode(',', $this->content),
               $favourites
            );
      }
      $i = 0;
      $content = implode('', array_map(function($type) use (&$i) {
         $registrar = Registrar::getInstance($type);
         $args = [
            'post_type' => $registrar->getBased(),
            'posts_per_page'  => 1,
            'fields' => 'ids',
         ];
         if (!is_null($this->context)) {
            $context = Registrar::getInstance($this->context);
            switch ($context->getType()) {
               case 'term':
                  $args['tax_query'] = [];
                  $args['tax_query'][] = [
                     'taxonomy'  => $context->getBased(),
                     'terms'     => $this->contextID
                  ];
                  break;
               case 'user':
                  $args['author'] = $this->contextID;
                  break;
            }
         }
         $i = 0;
         $content = implode('', array_map(function($type) use (&$i) {
            $registrar = Registrar::getInstance($type);
         $check = new WP_Query($args);
         $hasPosts = !empty($check->posts);
         wp_reset_postdata();
            $args = [
               'post_type' => $registrar->getBased(),
               'posts_per_page'  => 1,
               'fields' => 'ids',
            ];
            if (!is_null($this->context)) {
               $context = Registrar::getInstance($this->context);
               switch ($context->getType()) {
                  case 'term':
                     $args['tax_query'] = [];
                     $args['tax_query'][] = [
                        'taxonomy'  => $context->getBased(),
                        'terms'     => $this->contextID
                     ];
                     break;
                  case 'user':
                     $args['author'] = $this->contextID;
                     break;
               }
            }
            $check = new WP_Query($args);
            $hasPosts = !empty($check->posts);
            wp_reset_postdata();
            $disabled = !$hasPosts;
            $checked = $i === 0 && $hasPosts;
            if ($hasPosts) {
               $i++;
            }
            return sprintf(
               '<input type="radio"
         $disabled = !$hasPosts;
         $checked = $i === 0 && $hasPosts;
         if ($hasPosts) {
            $i++;
         }
         return sprintf(
            '<input type="radio"
               id="filter-%s"
               class="btn"
               name="content"
@@ -285,60 +303,60 @@
               data-label="%s"
               value="%s"%s%s>
               <label for="filter-%s" title="Show %s">%s<span class="label">%s</span></label>',
               $type,
               $registrar->getSingular(),
               $type,
               $checked ? ' checked' : '',
               $disabled ? ' disabled' : '',
               $type,
               $registrar->getSingular(),
               jvbIcon($registrar->getIcon()),
               $registrar->getSingular()
            );
         }, $this->content));
            $type,
            $registrar->getSingular(),
            $type,
            $checked ? ' checked' : '',
            $disabled ? ' disabled' : '',
            $type,
            $registrar->getSingular(),
            jvbIcon($registrar->getIcon()),
            $registrar->getSingular()
         );
      }, $this->content));
         return sprintf(
            '<div class="content row left nowrap"><span class="label">Showing:</span>
      return sprintf(
         '<div class="content row left nowrap"><span class="label">Showing:</span>
               %s%s
            </div>',
            $content,
            $favourites
         );
         $content,
         $favourites
      );
   }
   protected function renderFilters():string
   {
      if (empty ($this->taxonomies)) {
         return '';
      }
      $inside = implode('', array_filter(array_map(function($tax) {
         $registrar = Registrar::getInstance($tax);
         if (!$registrar) return '';
      protected function renderFilters():string
      {
         if (empty ($this->taxonomies)) {
            return '';
         }
         $inside = implode('', array_filter(array_map(function($tax) {
            $registrar = Registrar::getInstance($tax);
            if (!$registrar) return '';
         $current = BASE.$this->content[0];
         $contentFor = $registrar->registrar->for;
         $hidden = in_array($current, $contentFor) ? '' : ' hidden';
            $current = BASE.$this->content[0];
            $contentFor = $registrar->registrar->for;
            $hidden = in_array($current, $contentFor) ? '' : ' hidden';
         $selector = new TaxonomySelector(
            'feed-'.$tax,
            $tax,
            [
               'icon'   => $registrar->getIcon(),
               'update'=> '.selected-items-section .selected-items',
               'types'  => $contentFor,
               'autocomplete' => false,
               'hidden' => $hidden,
               'output' => 'minimal',
               'search' => true
            ]
         );
         return $selector->render();
            $selector = new TaxonomySelector(
               'feed-'.$tax,
               $tax,
               [
                  'icon'   => $registrar->getIcon(),
                  'update'=> '.selected-items-section .selected-items',
                  'types'  => $contentFor,
                  'autocomplete' => false,
                  'hidden' => $hidden,
                  'output' => 'minimal',
                  'search' => true
               ]
            );
            return $selector->render();
         }, $this->taxonomies)));
         return sprintf(
            '<div class="taxonomies row left">
      }, $this->taxonomies)));
      return sprintf(
         '<div class="taxonomies row left">
            <div class="row top wrap">
               <span class="label">Filter By:</span>
               %s
@@ -354,155 +372,155 @@
               </div>
            </div>
         </div>',
            $inside,
            str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])),
            jvbIcon('x')
         );
      }
         $inside,
         str_replace('class="toggle-text"', 'class="toggle-text" hidden', jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match'])),
         jvbIcon('x')
      );
   }
      protected function renderOrderControls():string
      {
         $orderby = [
            [
               'slug'   => 'title',
               'icon'   => 'alphabetical',
               'label'  => 'Name'
            ],
            [
               'slug'   => 'date',
               'icon'   => 'calendar',
               'label'  => 'Date Created',
            ],
            [
               'slug'   => 'date_modified',
               'icon'   => 'clock-clockwise',
               'label'  => 'Date Modified'
            ]
         ];
         $custom = $this->getCustomOrdering();
         $orderby = $orderby + $custom;
         $orderby[] = [
            'slug'   => 'random',
            'icon'   => 'shuffle',
            'label'  => 'Randomly'
         ];
   protected function renderOrderControls():string
   {
      $orderby = [
         [
            'slug'   => 'title',
            'icon'   => 'alphabetical',
            'label'  => 'Name'
         ],
         [
            'slug'   => 'date',
            'icon'   => 'calendar',
            'label'  => 'Date Created',
         ],
         [
            'slug'   => 'date_modified',
            'icon'   => 'clock-clockwise',
            'label'  => 'Date Modified'
         ]
      ];
      $custom = $this->getCustomOrdering();
      $orderby = $orderby + $custom;
      $orderby[] = [
         'slug'   => 'random',
         'icon'   => 'shuffle',
         'label'  => 'Randomly'
      ];
         $custom = implode(',', array_map(function($ord) {
            return $ord['slug'];
         }, $custom));
      $custom = implode(',', array_map(function($ord) {
         return $ord['slug'];
      }, $custom));
         $i = 0;
         $orderby = sprintf(
            '<div class="orderby row left">
      $i = 0;
      $orderby = sprintf(
         '<div class="orderby row left">
            <span class="label">Order by:</span>%s
            </div>',
            implode('', array_map(function ($by) use (&$i){
               $checked = $i === 0 ? ' checked' : '';
               $i++;
               return sprintf(
                  '<input type="radio" id="order-%s" class="btn" name="orderby" value="%s" data-filter="orderby"%s%s>
         implode('', array_map(function ($by) use (&$i){
            $checked = $i === 0 ? ' checked' : '';
            $i++;
            return sprintf(
               '<input type="radio" id="order-%s" class="btn" name="orderby" value="%s" data-filter="orderby"%s%s>
                     <label for="order-%s" title="Order %s">%s<span class="label">%s</span></label>',
                  $by['slug'],
                  $by['slug'],
                  $checked,
                  empty($by['for']??[]) ? '' : ' data-for="'.implode($by['for']).'"',
                  $by['slug'],
                  $by['slug'] === 'random' ? $by['label'] : 'by '.$by['label'],
                  jvbIcon($by['icon']),
                  $by['label']
               );
            }, $orderby))
         );
               $by['slug'],
               $by['slug'],
               $checked,
               empty($by['for']??[]) ? '' : ' data-for="'.implode($by['for']).'"',
               $by['slug'],
               $by['slug'] === 'random' ? $by['label'] : 'by '.$by['label'],
               jvbIcon($by['icon']),
               $by['label']
            );
         }, $orderby))
      );
         $order = [
            [
               'slug'   => 'desc',
               'icon'   => 'sort-descending',
               'label'  => 'Descending (A-Z, 1-10)'
            ],
            [
               'slug'   => 'asc',
               'icon'   => 'sort-ascending',
               'label'  => 'Ascending (Z-A, 10-1)'
            ]
         ];
      $order = [
         [
            'slug'   => 'desc',
            'icon'   => 'sort-descending',
            'label'  => 'Descending (A-Z, 1-10)'
         ],
         [
            'slug'   => 'asc',
            'icon'   => 'sort-ascending',
            'label'  => 'Ascending (Z-A, 10-1)'
         ]
      ];
         $i = 0;
         $order = sprintf(
            '<div class="order-direction row left" data-for-order="date,date_modified,title%s">
      $i = 0;
      $order = sprintf(
         '<div class="order-direction row left" data-for-order="date,date_modified,title%s">
            <span class="label">Order:</span>
            %s
            </div>',
            $custom === '' ? '' : ','.$custom,
            implode('', array_map(function ($ord) use (&$i) {
               $checked = $i=== 0 ? ' checked' : '';
               $i++;
               return sprintf(
                  '<input type="radio" id="order-%s" class="btn" name="order" value="%s" data-filter="order"%s>
         $custom === '' ? '' : ','.$custom,
         implode('', array_map(function ($ord) use (&$i) {
            $checked = $i=== 0 ? ' checked' : '';
            $i++;
            return sprintf(
               '<input type="radio" id="order-%s" class="btn" name="order" value="%s" data-filter="order"%s>
                  <label for="order-%s" title="Sort %s">
                     %s
                     <span class="label">%s</span>
                  </label>',
                  $ord['slug'],
                  $ord['slug'],
                  $checked,
                  $ord['slug'],
                  $ord['label'],
                  jvbIcon($ord['icon']),
                  $ord['label']
               );
            }, $order))
         );
               $ord['slug'],
               $ord['slug'],
               $checked,
               $ord['slug'],
               $ord['label'],
               jvbIcon($ord['icon']),
               $ord['label']
            );
         }, $order))
      );
         return sprintf(
            '<div class="ordering row top left nowrap">%s%s</div>',
            $orderby,
            $order
         );
      }
         protected function getCustomOrdering():array
         {
            $custom = [];
            foreach ($this->content as $content) {
               $registrar = Registrar::getInstance($content);
               if (!$registrar || empty($registrar->config('feed')->getCustomOrder())) {
                  continue;
               }
               $custom = array_merge_recursive($custom, $registrar->config('feed')->getCustomOrder());
            }
            return $custom;
      return sprintf(
         '<div class="ordering row top left nowrap">%s%s</div>',
         $orderby,
         $order
      );
   }
   protected function getCustomOrdering():array
   {
      $custom = [];
      foreach ($this->content as $content) {
         $registrar = Registrar::getInstance($content);
         if (!$registrar || empty($registrar->config('feed')->getCustomOrder())) {
            continue;
         }
      protected function renderViewControls():string
      {
         $views = [
            'grid'   => ['slug' => 'grid', 'icon' => 'squares-four', 'label' => 'Grid View'],
            'list'   => ['slug' => 'list', 'icon' => 'rows', 'label' => 'List View']
         ];
         $custom = array_merge_recursive($custom, $registrar->config('feed')->getCustomOrder());
      }
      return $custom;
   }
   protected function renderViewControls():string
   {
      $views = [
         'grid'   => ['slug' => 'grid', 'icon' => 'squares-four', 'label' => 'Grid View'],
         'list'   => ['slug' => 'list', 'icon' => 'rows', 'label' => 'List View']
      ];
         $i = 0;
         return sprintf(
            '<div class="view row left nowrap"><span class="label">Switch View:</span>%s</div>',
            implode('', array_map(function ($view) use (&$i) {
               $checked = $i === 0 ? ' checked' : '';
               $i++;
               return sprintf(
                  '<input type="radio"
      $i = 0;
      return sprintf(
         '<div class="view row left nowrap"><span class="label">Switch View:</span>%s</div>',
         implode('', array_map(function ($view) use (&$i) {
            $checked = $i === 0 ? ' checked' : '';
            $i++;
            return sprintf(
               '<input type="radio"
                  data-view="%s" value="%s" class="btn" name="view" id="view-%s"%s>
                  <label for="view-%s" title="%s">
                  %s<span class="label">%s</span>
</label>',
                  $view['slug'],
                  $view['slug'],
                  $view['slug'],
                  $checked,
                  $view['slug'],
                  $view['label'],
                  jvbIcon($view['icon']),
                  $view['label'],
               );
            }, $views))
         );
      }
               $view['slug'],
               $view['slug'],
               $view['slug'],
               $checked,
               $view['slug'],
               $view['label'],
               jvbIcon($view['icon']),
               $view['label'],
            );
         }, $views))
      );
   }
   protected function renderPlaceholders():string
   {
@@ -542,37 +560,37 @@
      $items = $this->getItems();
      $out = '<div class="item-grid">';
         $out .= implode('',array_map(function ($ID) {
            $content = $this->isContentTax
               ? jvbNoBase($this->content[0])
               : jvbNoBase(get_post_type($ID));
      $out .= implode('',array_map(function ($ID) {
         $content = $this->isContentTax
            ? jvbNoBase($this->content[0])
            : jvbNoBase(get_post_type($ID));
            return $this->renderItem($content, $ID);
         }, $items));
         return $this->renderItem($content, $ID);
      }, $items));
      $out .= '</div>';
      return $out;
   }
      protected function getItems():array
      {
         if ($this->isContentTax) {
            $items = get_terms([
               'taxonomy'  => $this->content,
               'fields' => 'ids',
               'meta_key'  => BASE.'date_modified',
               'meta_type' => 'DATETIME',
               'orderby'   => 'meta_value',
               'order'     => 'desc',
               'number' => $this->args['posts_per_page']
            ]);
            $items = $items && !is_wp_error($items) ? $items : [];
         } else {
            $items = new WP_Query($this->args);
            $this->hasMore = $items->found_posts > $this->args['posts_per_page'];
            $items = $items->posts;
            wp_reset_postdata();
         }
         return $items;
   protected function getItems():array
   {
      if ($this->isContentTax) {
         $items = get_terms([
            'taxonomy'  => $this->content,
            'fields' => 'ids',
            'meta_key'  => BASE.'date_modified',
            'meta_type' => 'DATETIME',
            'orderby'   => 'meta_value',
            'order'     => 'desc',
            'number' => $this->args['posts_per_page']
         ]);
         $items = $items && !is_wp_error($items) ? $items : [];
      } else {
         $items = new WP_Query($this->args);
         $this->hasMore = $items->found_posts > $this->args['posts_per_page'];
         $items = $items->posts;
         wp_reset_postdata();
      }
      return $items;
   }
   protected function renderLoader():string
   {
@@ -705,76 +723,76 @@
         $out
      );
   }
      protected function buildFeedItem(string $content, ?int $ID = null):string
      {
         $registrar = Registrar::getInstance($content);
         $meta = is_null($ID) ? false :
            match ($registrar->getType()) {
               'post'   => Meta::forPost($ID),
               'term'   => Meta::forTerm($ID),
               'user'   => Meta::forUser($ID),
               default => false
            };
   protected function buildFeedItem(string $content, ?int $ID = null):string
   {
      $registrar = Registrar::getInstance($content);
      $meta = is_null($ID) ? false :
         match ($registrar->getType()) {
            'post'   => Meta::forPost($ID),
            'term'   => Meta::forTerm($ID),
            'user'   => Meta::forUser($ID),
            default => false
         };
         [$images, $fields] = $registrar->getFeedFields();
      [$images, $fields] = $registrar->getFeedFields();
         /**
          * Get the main image for the feed item.
          * Output can be overridden with the $imagesFn
          */
         $imagesFn = BASE.'render_'.$content.'_feed_item_images';
         if (function_exists($imagesFn)) {
            $img = $imagesFn($ID, $images);
         } else {
            $img = '';
            foreach ($images as $config) {
               $field = $config['name'];
               $img .= $meta ? jvbFormatImage($meta->get($field), 'tiny', 'medium')
                  : $this->defaultFieldTemplate($config['type'], $field);
      /**
       * Get the main image for the feed item.
       * Output can be overridden with the $imagesFn
       */
      $imagesFn = BASE.'render_'.$content.'_feed_item_images';
      if (function_exists($imagesFn)) {
         $img = $imagesFn($ID, $images);
      } else {
         $img = '';
         foreach ($images as $config) {
            $field = $config['name'];
            $img .= $meta ? jvbFormatImage($meta->get($field), 'tiny', 'medium')
               : $this->defaultFieldTemplate($config['type'], $field);
         }
      }
      $img = sprintf(
         '<div class="images"><a href="%s">%s</a></div>',
         $ID ? get_the_permalink($ID) : '',
         $img
      );
      /**
       * Start the Details with the fields
       * Plugins can modify the summary title with the 'jvbFeedItemSummary' filter
       */
      $summary = sprintf(
         '<details><summary>%s</summary>',
         apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
      );
      /**
       * Work through the fields
       * Each field output can be overridden with $field or $fieldType
       */
      foreach ($fields as $config) {
         $f = $config['name'];
         $functions = [
            BASE.'render_'.$content.'_field_'.$f,  //Overrides field for this content
            BASE.'render_field_'.$f,            //Overrides field with this name
            BASE.'render_field_type_'.$config['type'] //Overrides field of this type
         ];
         $didIt = false;
         foreach ($functions as $func) {
            if (!$didIt && function_exists($func)) {
               $didIt = true;
               $summary .= $func($ID, is_null($ID));
            }
         }
         $img = sprintf(
            '<div class="images"><a href="%s">%s</a></div>',
            $ID ? get_the_permalink($ID) : '',
            $img
         );
         if (!$didIt) {
            $summary .= $this->defaultFieldTemplate($config['type'], $f, is_null($ID), $meta, $config);
         }
         /**
          * Start the Details with the fields
          * Plugins can modify the summary title with the 'jvbFeedItemSummary' filter
          */
         $summary = sprintf(
            '<details><summary>%s</summary>',
            apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content)
         );
            /**
             * Work through the fields
             * Each field output can be overridden with $field or $fieldType
             */
            foreach ($fields as $config) {
               $f = $config['name'];
               $functions = [
                  BASE.'render_'.$content.'_field_'.$f,  //Overrides field for this content
                  BASE.'render_field_'.$f,            //Overrides field with this name
                  BASE.'render_field_type_'.$config['type'] //Overrides field of this type
               ];
               $didIt = false;
               foreach ($functions as $func) {
                  if (!$didIt && function_exists($func)) {
                     $didIt = true;
                     $summary .= $func($ID, is_null($ID));
                  }
               }
               if (!$didIt) {
                  $summary .= $this->defaultFieldTemplate($config['type'], $f, is_null($ID), $meta, $config);
               }
            }
         $summary .= '</details>';
         return $img.$summary;
      }
      $summary .= '</details>';
      return $img.$summary;
   }
   protected function defaultFieldTemplate(string $fieldType, string $fieldName, bool $isTemplate = true, bool|Meta $meta = false, array $config = []):string
   {
@@ -787,7 +805,7 @@
         case 'post_title':
            return sprintf(
               '<h3%s>%s</h3>',
         $data,
               $data,
               $meta && !empty($value) ? $value : '',
            );
         case 'post_date':