From a9580bb064babca65ace90c73ae4e0c54be6ef14 Mon Sep 17 00:00:00 2001 From: EarthlingDavey <15802017+EarthlingDavey@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:10:42 +0100 Subject: [PATCH] AJAX News & Blogs filtering complete --- public/app/themes/clarity/functions.php | 5 +- .../clarity/inc/api/get-news-rest-api.php | 2 +- .../clarity/inc/api/get-posts-rest-api.php | 41 --- .../clarity/inc/content-filter/search.php | 264 ++++++++++-------- .../clarity/inc/pagination-newscategory.php | 54 ---- public/app/themes/clarity/inc/pagination.php | 4 +- public/app/themes/clarity/page_blog.php | 30 +- public/app/themes/clarity/page_news.php | 45 +-- .../src/components/c-article-item/style.styl | 33 ++- .../c-article-item/view-blog-feed.ajax.php | 42 +++ .../c-article-item/view-blog-feed.php | 76 +++-- ...s-feed.tpl.php => view-news-feed.ajax.php} | 10 +- .../c-article-item/view-news-feed.php | 34 +-- .../clarity/src/components/c-article/view.php | 27 +- .../c-content-filter/view-events.php | 14 +- .../src/components/c-content-filter/view.php | 20 +- .../src/components/c-news-article/view.php | 26 +- .../c-pagination/view-infinite.ajax.php} | 4 +- .../components/c-pagination/view-infinite.php | 41 +++ .../clarity/src/globals/js/ajax-filter.js | 205 ++++++++++++++ .../clarity/src/globals/js/ajax-templating.js | 87 ++++++ .../src/globals/js/blog-content_filter.js | 227 --------------- .../clarity/src/globals/js/script-loader.js | 2 +- 23 files changed, 718 insertions(+), 575 deletions(-) delete mode 100644 public/app/themes/clarity/inc/api/get-posts-rest-api.php delete mode 100644 public/app/themes/clarity/inc/pagination-newscategory.php create mode 100644 public/app/themes/clarity/src/components/c-article-item/view-blog-feed.ajax.php rename public/app/themes/clarity/src/components/c-article-item/{view-news-feed.tpl.php => view-news-feed.ajax.php} (82%) rename public/app/themes/clarity/{inc/pagination.tpl.php => src/components/c-pagination/view-infinite.ajax.php} (82%) create mode 100644 public/app/themes/clarity/src/components/c-pagination/view-infinite.php create mode 100644 public/app/themes/clarity/src/globals/js/ajax-filter.js create mode 100644 public/app/themes/clarity/src/globals/js/ajax-templating.js delete mode 100644 public/app/themes/clarity/src/globals/js/blog-content_filter.js diff --git a/public/app/themes/clarity/functions.php b/public/app/themes/clarity/functions.php index 9763e6f9b..b5ce4e6c0 100644 --- a/public/app/themes/clarity/functions.php +++ b/public/app/themes/clarity/functions.php @@ -59,7 +59,6 @@ require_once 'inc/amazon-s3-and-cloudfront-signing.php'; require_once 'inc/amazon-s3-and-cloudfront.php'; -require_once 'inc/api/get-posts-rest-api.php'; require_once 'inc/api/campaign-api.php'; require_once 'inc/api/get-campaign-posts-api.php'; require_once 'inc/api/get-news-rest-api.php'; @@ -94,7 +93,6 @@ require_once 'inc/menu.php'; require_once 'inc/utilities.php'; require_once 'inc/pagination.php'; -require_once 'inc/pagination-newscategory.php'; require_once 'inc/post-types/post.php'; require_once 'inc/post-types/event.php'; @@ -120,6 +118,9 @@ new MOJ\Intranet\WPOffloadMedia(); new MOJ\Intranet\WPElasticPress(); +$search = new MOJ\Intranet\Search(); +$search->hooks(); + /// Prevent the Agency Switcher page from being overwritten add_action('save_post', function ($post_id, $post) { if ($post->post_name === 'agency-switcher') { diff --git a/public/app/themes/clarity/inc/api/get-news-rest-api.php b/public/app/themes/clarity/inc/api/get-news-rest-api.php index 2e21c13ce..d28f654cc 100644 --- a/public/app/themes/clarity/inc/api/get-news-rest-api.php +++ b/public/app/themes/clarity/inc/api/get-news-rest-api.php @@ -17,7 +17,7 @@ function get_news_api($set_cpt = '') $oAgency = new Agency(); $activeAgency = $oAgency->getCurrentAgency(); $post_per_page = 10; - $post_type = 'news'; + $post_type = get_post_type(); $post_id = get_the_ID(); $region_id = get_the_terms($post_id, 'region'); $regional_template = get_post_meta(get_the_ID(), 'dw_regional_template', true); diff --git a/public/app/themes/clarity/inc/api/get-posts-rest-api.php b/public/app/themes/clarity/inc/api/get-posts-rest-api.php deleted file mode 100644 index a8628adb9..000000000 --- a/public/app/themes/clarity/inc/api/get-posts-rest-api.php +++ /dev/null @@ -1,41 +0,0 @@ -getCurrentAgency(); - - $args = [ - 'numberposts' => $blog_posts_number, - 'post_type' => 'post', - 'post_status' => 'publish', - 'tax_query' => [ - 'relation' => 'AND', - [ - 'taxonomy' => 'agency', - 'field' => 'term_id', - 'terms' => $activeAgency['wp_tag_id'] - ], - ] - ]; - - $posts = get_posts($args); - - echo '
'; - foreach ($posts as $key => $post) { - include locate_template('src/components/c-article-item/view-blog-feed.php'); - } - -} diff --git a/public/app/themes/clarity/inc/content-filter/search.php b/public/app/themes/clarity/inc/content-filter/search.php index ddd279c62..4a58fd3e9 100644 --- a/public/app/themes/clarity/inc/content-filter/search.php +++ b/public/app/themes/clarity/inc/content-filter/search.php @@ -24,47 +24,86 @@ * * @property string $agency - The active agency. * @property string $post_type - The post type. - * @property string $page - The page number. - * @property string $posts_per_page - The number of posts per page. - * @property string $keywords_filter - The keywords filter. - * @property string $date_filter - The date filter. - * + * @property int $page - The page number. + * @property int $posts_per_page - The number of posts per page. + * @property ?bool $exclude_current - Exclude the current post. + * @property ?string $keywords_filter - The keywords filter. + * @property ?string $date_filter - The date filter. + * @property ?string $news_category_id - The news category ID. + * @property ?string $region_id - The region ID. + * * @return void */ -class QueryProps +class SearchQueryArgs { - public $agency_term_id; - public $post_type; - public $page; - public $posts_per_page; - public $keywords_filter; - public $date_filter; - public $news_category_id; - public $region_id; - public function __construct( - $agency_term_id, - $post_type, - $page, - $posts_per_page = 10, - $keywords_filter = null, - $date_filter = null, - $news_category_id = null, - $region_id = null - ) { - $this->agency_term_id = $agency_term_id; - $this->post_type = $post_type; - $this->page = $page; - $this->posts_per_page = $posts_per_page; - $this->date_filter = $date_filter; - $this->keywords_filter = $keywords_filter; - $this->news_category_id = $news_category_id; - $this->region_id = $region_id; + public string $agency_term_id, + public string $post_type, + public int $page, + public int $posts_per_page = 10, + public ?bool $exclude_current = false, + public ?string $keywords_filter = null, + public ?string $date_filter = null, + public ?string $news_category_id = null, + public ?string $region_id = null, + ) {} + + function get() + { + // Pagination. + $offset = $this->page ? (($this->page - 1) * $this->posts_per_page) : 0; + + $args = [ + 'posts_per_page' => $this->posts_per_page, + 'post_type' => $this->post_type, + 'post_status' => 'publish', + 'offset' => $offset, + ...($this->exclude_current ? ['post__not_in' => [get_the_ID()]] : []), + 'tax_query' => [ + 'relation' => 'AND', + [ + 'taxonomy' => 'agency', + 'field' => 'term_id', + 'terms' => $this->agency_term_id + ], + // If the region is set add its ID to the taxonomy query + ...(!empty($this->region_id) ? [ + 'taxonomy' => 'region', + 'field' => 'region_id', + 'terms' => $this->region_id, + ] : []), + // If the news category is set add its ID unless the query is regional, + // as it will have already been added to the tax query. + ...(!empty($this->news_category_id) && empty($this->region_id) ? [ + 'taxonomy' => 'news_category', + 'field' => 'category_id', + 'terms' => $this->news_category_id, + ] : []), + ] + ]; + + // Parse dates from the date filter. + if (!empty($this->date_filter)) { + preg_match('/&after=([^&]*)&before=([^&]*)/', $this->date_filter, $matches); + $args['date_query'] = [ + 'after' => date('Y-m-d', strtotime($matches[1])), + 'before' => date('Y-m-d', strtotime($matches[2])), + 'inclusive' => false, + ]; + } + + // If there is a search query, set the orderby to relevance. + if (!empty($this->keywords_filter)) { + $args['orderby'] = 'relevance'; + $args['s'] = $this->keywords_filter; + } + + return $args; } } -class FilterSearch +class Search { /** @@ -73,10 +112,7 @@ class FilterSearch * @return void */ - public function __construct() - { - $this->hooks(); - } + public function __construct() {} /** * Hooks @@ -85,10 +121,14 @@ public function __construct() */ public function hooks(): void { + // Add functions to handle AJAX requests. add_action('wp_ajax_load_search_results', [$this, 'loadSearchResults']); add_action('wp_ajax_nopriv_load_search_results', [$this, 'loadSearchResults']); add_action('wp_ajax_load_events_filter_results', [$this, 'loadEventSearchResults']); add_action('wp_ajax_nopriv_load_events_filter_results', [$this, 'loadEventSearchResults']); + + // Add templates to the footer. + add_action('wp_footer', [$this, 'addAjaxTemplates']); } /** @@ -164,20 +204,49 @@ public function loadEventSearchResults() die(); } - public function mapResults(\WP_Post $post) + public function mapNewsResult(\WP_Post $post) { return [ 'ID' => $post->ID, + 'post_type' => get_post_type($post->ID), 'post_title' => $post->post_title, 'post_date_formatted' => get_gmt_from_date($post->post_date, 'j M Y'), 'post_excerpt_formatted' => empty($post->post_excerpt) ? '' : "

{$post->post_excerpt}

", 'permalink' => get_permalink($post->ID), - 'post_type' => get_post_type($post->ID), // ? Is not used in the template. 'post_thumbnail' => get_the_post_thumbnail_url($post->ID, 'user-thumb'), 'post_thumbnail_alt' => get_post_meta(get_post_thumbnail_id($post->ID), '_wp_attachment_image_alt', true), ]; } + public function mapPostResult(\WP_Post $post) + { + + $thumbnail = get_the_post_thumbnail_url($post->ID, 'user-thumb'); + $thumbnail_alt = get_post_meta(get_post_thumbnail_id($post->ID), '_wp_attachment_image_alt', true); + + $author = $post->post_author; + $author_display_name = $author ? get_the_author_meta('display_name', $author) : ''; + + if (!$thumbnail) { + // Mutate thumbnail with author image. + $thumbnail = $author ? get_the_author_meta('thumbnail_avatar', $author) : false; + $thumbnail_alt = $author_display_name; + } + + return [ + 'ID' => $post->ID, + 'post_type' => get_post_type($post->ID), + 'post_title' => $post->post_title, + 'post_date_formatted' => get_gmt_from_date($post->post_date, 'j M Y'), + 'post_excerpt_formatted' => empty($post->post_excerpt) ? '' : "

{$post->post_excerpt}

", + 'permalink' => get_permalink($post->ID), + // 'post_thumbnail' => $thumbnail, + 'post_thumbnail' => false, + 'post_thumbnail_alt' => $thumbnail_alt, + 'author_display_name' => $author ? get_the_author_meta('display_name', $author) : '', + ]; + } + /** * Load results for post types except for events. * @@ -197,20 +266,29 @@ public function loadSearchResults() add_filter('ep_enable_do_weighting', '__return_true'); $page = (int) $_POST['page'] ?? 1; - if($page < 1 || $page > 1000) { + if ($page < 1 || $page > 1000) { $page = 1; } $posts_per_page = (int) ($_POST['posts_per_page'] ?? 10); - if($posts_per_page < 1 || $posts_per_page > 100) { + if ($posts_per_page < 1 || $posts_per_page > 100) { $posts_per_page = 10; } - $query_props = new QueryProps( + $allowed_post_types = ['post', 'news']; + + if (!in_array($_POST['post_type'], $allowed_post_types)) { + throw new \Exception('Invalid post type.'); + } + + $post_type = $_POST['post_type']; + + $query_args = new SearchQueryArgs( (new Agency())->getCurrentAgency()['wp_tag_id'], - sanitize_text_field($_POST['post_type']), + $post_type, $page, $posts_per_page, + false, sanitize_text_field($_POST['keywords_filter'] ?? null), sanitize_text_field($_POST['date_filter'] ?? null), sanitize_text_field($_POST['news_category_id'] ?? null), @@ -218,9 +296,9 @@ public function loadSearchResults() ); // Run a query based on generated query arguments. - $query = new WP_Query($this->getQueryArgs($query_props)); + $query = new WP_Query($query_args->get()); - // include locate_template('src/components/c-article-item/view-news-feed.php'); + $map_function = $post_type === 'news' ? 'mapNewsResult' : 'mapPostResult'; return wp_send_json([ 'aggregates' => [ @@ -228,98 +306,36 @@ public function loadSearchResults() 'resultsPerPage' => $posts_per_page, 'currentPage' => $page, ], - 'results' => array_map([$this, 'mapResults'], $query->posts), + 'results' => [ + 'posts' => array_map([$this, $map_function], $query->posts), + 'templateName' => "view-{$post_type}-feed", + ], ]); - } + /** - * Get Query Args + * Add AJAX templates to the footer. * - * @return array + * These JS templates are used to render the AJAX results to html. + * + * @return void */ - function getQueryArgs(QueryProps $props) + public function addAjaxTemplates() { - // Pagination. - $offset = $props->page ? (($props->page - 1) * $props->posts_per_page) : 0; - $args = [ - 'numberposts' => $props->posts_per_page, - 'post_type' => $props->post_type, - 'post_status' => 'publish', - 'offset' => $offset, - 'tax_query' => [ - 'relation' => 'AND', - [ - 'taxonomy' => 'agency', - 'field' => 'term_id', - 'terms' => $props->agency_term_id - ], - // If the region is set add its ID to the taxonomy query - ...(!empty($props->region_id) ? [ - 'taxonomy' => 'region', - 'field' => 'region_id', - 'terms' => $props->region_id, - ] : []), - // If the news category is set add its ID unless the query is regional, - // as it will have already been added to the tax query. - ...(!empty($props->news_category_id) && empty($props->region_id) ? [ - 'taxonomy' => 'news_category', - 'field' => 'category_id', - 'terms' => $props->news_category_id, - ] : []), - ] - ]; - - // Parse dates from the date filter. - if (!empty($props->date_filter)) { - preg_match('/&after=([^&]*)&before=([^&]*)/', $props->date_filter, $matches); - $args['date_query'] = [ - 'after' => date('Y-m-d', strtotime($matches[1])), - 'before' => date('Y-m-d', strtotime($matches[2])), - 'inclusive' => false, - ]; + if (is_page_template('page_blog.php') || is_page_template('page_news.php')) { + get_template_part('src/components/c-pagination/view-infinite.ajax'); + echo ''; } - // If there is a search query, set the orderby to relevance. - if (!empty($props->keywords_filter)) { - $args['orderby'] = 'relevance'; - $args['s'] = $props->keywords_filter; + if (is_page_template('page_blog.php')) { + get_template_part('src/components/c-article-item/view-blog-feed.ajax'); } - return $args; - } - - /** - * Get Pagination - * - * @param string $selected - * @param int|string $next - * @param int $total - * @return string - */ - - function getPagination(string $selected, int|string $next, int $total): string - { - $html = ''; - if ($next == $total) { - $html .= ''; - $html .= 'No More Results'; - $html .= ''; - } elseif ($total <= 1) { - $html .= ''; - } else { - $html .= ''; + if (is_page_template('page_news.php')) { + get_template_part('src/components/c-article-item/view-news-feed.ajax'); } - return $html; } } - -new FilterSearch(); diff --git a/public/app/themes/clarity/inc/pagination-newscategory.php b/public/app/themes/clarity/inc/pagination-newscategory.php deleted file mode 100644 index e914a6ea0..000000000 --- a/public/app/themes/clarity/inc/pagination-newscategory.php +++ /dev/null @@ -1,54 +0,0 @@ -getCurrentAgency(); - - $post_per_page = 10; - - $args = [ - 'numberposts' => $post_per_page, - 'post_type' => 'news', - 'post_status' => 'publish', - 'tax_query' => [ - 'relation' => 'AND', - [ - 'taxonomy' => 'agency', - 'field' => 'term_id', - 'terms' => $activeAgency['wp_tag_id'] - ], - // If the category_id is set add it to the taxonomy query - ...( $category_id ? [ - 'taxonomy' => 'news_category', - 'field' => 'category_id', - 'terms' => $category_id, - ] : []), - ] - ]; - - $query = new WP_Query($args); - $pagetotal = $query->max_num_pages; - - ?> -
- - 0) { ?> - - diff --git a/public/app/themes/clarity/page_blog.php b/public/app/themes/clarity/page_blog.php index 0ad71d22e..82508d1d7 100644 --- a/public/app/themes/clarity/page_blog.php +++ b/public/app/themes/clarity/page_blog.php @@ -1,31 +1,45 @@ getCurrentAgency(); +// Use the SearchQueryProps class to create the query properties. +$query_args = new SearchQueryArgs((new Agency())->getCurrentAgency()['wp_tag_id'], 'post', 1, 10); + +// Run the query. +$query = new WP_Query($query_args->get()); ?>
+

+
- + 'post', 'template' => 'view-news-feed']); ?>
+

Latest

+ +
- + posts as $key => $post) { + include locate_template('src/components/c-article-item/view-blog-feed.php'); + } ?>
- + + $query->max_num_pages, 'page' => 1]); ?> +
+
getCurrentAgency(); +// Use the SearchQueryProps class to create the query properties. +$query_args = new SearchQueryArgs((new Agency())->getCurrentAgency()['wp_tag_id'], 'news', 1, 10); -// TODO: Add this to a function later -get_template_part('src/components/c-article-item/view-news-feed.tpl'); -// TODO: Add this to a function later -require_once 'inc/pagination.tpl.php'; +// Run the query. +$query = new WP_Query($query_args->get()); ?> -
-

-
- 'news', 'template' => 'view-news-feed-template']); ?> -
-
-

Latest

-
- -
- + +
+ +

+ +
+ 'news', 'template' => 'view-news-feed']); ?> +
+ +
+ +

Latest

+ +
+ posts as $key => $post) { + include locate_template('src/components/c-article-item/view-news-feed.php'); + } ?>
-
+ + $query->max_num_pages, 'page' => 1]); ?> + +
+
+ + diff --git a/public/app/themes/clarity/src/components/c-article-item/view-blog-feed.php b/public/app/themes/clarity/src/components/c-article-item/view-blog-feed.php index f749911b7..af79a0ec2 100644 --- a/public/app/themes/clarity/src/components/c-article-item/view-blog-feed.php +++ b/public/app/themes/clarity/src/components/c-article-item/view-blog-feed.php @@ -9,73 +9,63 @@ use MOJ\Intranet\Authors; -$oAuthor = new Authors(); $id = $post->ID; -$authors = $oAuthor->getAuthorInfo($id); + $thumbnail = get_the_post_thumbnail_url($id, 'user-thumb'); $thumbnail_alt = get_post_meta(get_post_thumbnail_id($id), '_wp_attachment_image_alt', true); -$link = get_the_permalink($id); + +// TODO: Why is this here? It's not used. +// If it should be here, make sure to update `Search->mapPostResult()` +$oAuthor = new Authors(); +$authors = $oAuthor->getAuthorInfo($id); + $author = $post->post_author; $author_display_name = $author ? get_the_author_meta('display_name', $author) : ''; -$author_avatar = $author ? get_the_author_meta('thumbnail_avatar', $author) : ''; - -// Filter right-hand blog list so the page your on isn't duplicated and doesn't appear in that list -if (is_singular('post')) { - $post_id = $post_id ?? 0; - if ($post_id === $id) { - $id = ''; - } -} -if ($id != '') : - ?> -
+if (!$thumbnail) { + // Mutate thumbnail with author image. + $thumbnail = $author ? get_the_author_meta('thumbnail_avatar', $author) : false; + $thumbnail_alt = $author_display_name; +} - +?> - +
- - + + - - '; ?> +
- +

- +

+ By + | + +
- + + -
-

-
+
+

+
-
- +
\ No newline at end of file diff --git a/public/app/themes/clarity/src/components/c-article-item/view-news-feed.tpl.php b/public/app/themes/clarity/src/components/c-article-item/view-news-feed.ajax.php similarity index 82% rename from public/app/themes/clarity/src/components/c-article-item/view-news-feed.tpl.php rename to public/app/themes/clarity/src/components/c-article-item/view-news-feed.ajax.php index a921e6f8d..79444e11c 100644 --- a/public/app/themes/clarity/src/components/c-article-item/view-news-feed.tpl.php +++ b/public/app/themes/clarity/src/components/c-article-item/view-news-feed.ajax.php @@ -1,7 +1,7 @@ +defined('ABSPATH') || exit; - +?> - \ No newline at end of file + diff --git a/public/app/themes/clarity/src/components/c-article-item/view-news-feed.php b/public/app/themes/clarity/src/components/c-article-item/view-news-feed.php index 494199581..a0796597f 100644 --- a/public/app/themes/clarity/src/components/c-article-item/view-news-feed.php +++ b/public/app/themes/clarity/src/components/c-article-item/view-news-feed.php @@ -18,17 +18,11 @@
- '; - echo '' . $thumbnail_alt . ''; - echo ''; - else : - echo ''; - endif; - - ?> + + +
@@ -37,18 +31,16 @@
- +
- -
- - -
-

-
- - + +
+

+
+
diff --git a/public/app/themes/clarity/src/components/c-article/view.php b/public/app/themes/clarity/src/components/c-article/view.php index 721a7e64c..6fbe8ddca 100644 --- a/public/app/themes/clarity/src/components/c-article/view.php +++ b/public/app/themes/clarity/src/components/c-article/view.php @@ -5,6 +5,28 @@ * */ + +use MOJ\Intranet\Agency; +use MOJ\Intranet\SearchQueryArgs; + +/* +* Template Name: Blog archive +*/ + +if (!defined('ABSPATH')) { + die(); +} + +defined('ABSPATH') || exit; + +get_header(); + +// Use the SearchQueryProps class to create the query properties. +$query_args = new SearchQueryArgs((new Agency())->getCurrentAgency()['wp_tag_id'], 'post', 1, 5, true); + +// Run the query. +$query = new WP_Query($query_args->get()); + ?>
@@ -22,8 +44,9 @@ diff --git a/public/app/themes/clarity/src/components/c-content-filter/view-events.php b/public/app/themes/clarity/src/components/c-content-filter/view-events.php index a944ebf5e..e8f5d4638 100644 --- a/public/app/themes/clarity/src/components/c-content-filter/view-events.php +++ b/public/app/themes/clarity/src/components/c-content-filter/view-events.php @@ -9,7 +9,7 @@
-
+

Filter by

@@ -35,6 +35,18 @@ // Hidden field to pass nonce for improved security form_builder('hidden', '', false, '_nonce', '_search_filter_wpnonce', $nonce, null, null, false, null, null); ?> + + + + + + + + + +
diff --git a/public/app/themes/clarity/src/components/c-content-filter/view.php b/public/app/themes/clarity/src/components/c-content-filter/view.php index d1a014125..b42dc8680 100644 --- a/public/app/themes/clarity/src/components/c-content-filter/view.php +++ b/public/app/themes/clarity/src/components/c-content-filter/view.php @@ -1,15 +1,9 @@ getCurrentAgency(); -$agency = $activeAgency['shortcode']; - $archives_args = [ 'type' => 'monthly', 'format' => 'custom', @@ -33,16 +27,7 @@ - + - + + diff --git a/public/app/themes/clarity/src/components/c-news-article/view.php b/public/app/themes/clarity/src/components/c-news-article/view.php index d65a5c932..19156c083 100644 --- a/public/app/themes/clarity/src/components/c-news-article/view.php +++ b/public/app/themes/clarity/src/components/c-news-article/view.php @@ -1,3 +1,24 @@ +getCurrentAgency()['wp_tag_id'], 'news', 1, 6, true); + +// Run the query. +$query = new WP_Query($query_args->get()); + +?> +
@@ -16,8 +37,9 @@ Recent news'; - $news_posts_per_page = ''; - get_news_api(); + foreach ($query->posts as $key => $post) { + include locate_template('src/components/c-article-item/view-news-feed.php'); + } ?> diff --git a/public/app/themes/clarity/inc/pagination.tpl.php b/public/app/themes/clarity/src/components/c-pagination/view-infinite.ajax.php similarity index 82% rename from public/app/themes/clarity/inc/pagination.tpl.php rename to public/app/themes/clarity/src/components/c-pagination/view-infinite.ajax.php index 10f1eb11e..c6aaafe78 100644 --- a/public/app/themes/clarity/inc/pagination.tpl.php +++ b/public/app/themes/clarity/src/components/c-pagination/view-infinite.ajax.php @@ -1,10 +1,10 @@ $query->max_num_pages, 'page' => 1]); + * + * @package Clarity + */ + +defined('ABSPATH') || exit; + +if (empty($args['total_pages']) || empty($args['page'])) { + return; +} + +?> + + + diff --git a/public/app/themes/clarity/src/globals/js/ajax-filter.js b/public/app/themes/clarity/src/globals/js/ajax-filter.js new file mode 100644 index 000000000..059e7bd7b --- /dev/null +++ b/public/app/themes/clarity/src/globals/js/ajax-filter.js @@ -0,0 +1,205 @@ +import AjaxTemplating from "./ajax-templating.js"; + +export default (function ($) { + /** + * Render the response to the page. + * + * The results type should match the php response object + * as returned by the `FilterSearch->mapResults()` method. + * @typedef {Object} Post + * @property {string} ID + * @property {string} post_title + * @property {string} post_date_formatted + * @property {string} post_excerpt_formatted + * @property {string} permalink + * @property {string} post_type + * @property {string} post_thumbnail + * @property {string} post_thumbnail_alt + * + * @typedef {Object} Results + * @property {Post[]} posts + * @property {string} templateName + * + * The response type should match the php response object + * as returned by the `FilterSearch->loadSearchResults()` method. + * @typedef {Object} Response + * @property {Object} aggregates + * @property {number} aggregates.currentPage + * @property {number} aggregates.resultsPerPage + * @property {number} aggregates.totalResults + * @property {Results} results + * + * @param {Response} response + */ + + const renderResults = ({ + results: { posts, templateName }, + aggregates: { currentPage, totalResults }, + }) => { + // Remove all articles if page is 1. + if (currentPage === 1) { + $(".c-article-item").remove(); + } + + const t = new AjaxTemplating(templateName); + + const resultsHtml = posts.map((props) => t.renderHtml(props)); + + // Append the html to the content section. + $("#content").append(resultsHtml); + + // Update the title. + $("#title-section").text(`${totalResults} search results`); + }; + + /** + * Render pagination to the page. + */ + + const renderPagination = ({ currentPage, resultsPerPage, totalResults }) => { + const isLastPage = currentPage * resultsPerPage >= totalResults; + + const paginationTitle = ({ totalResults, isLastPage }) => { + if (!totalResults) { + return "No Results"; + } + if (isLastPage) { + return "No More Results"; + } + return `Load Next ${resultsPerPage} Results`; + }; + + const template = new AjaxTemplating("pagination"); + + // Update the pagination. + const paginationHtml = template.renderHtml({ + title: paginationTitle({ totalResults, isLastPage }), + // Disable the button if it's the last page. + disabled: isLastPage ? `disabled="disabled"` : "", + // Adjust the zero-indexed current page. + currentPageFormatted: parseInt(currentPage), + // Calculate the total pages - if no results, then set as 1 to render '1 of 1'. + totalPages: !totalResults ? 1 : Math.ceil(totalResults / resultsPerPage), + }); + + $(".c-pagination").html(paginationHtml); + + // Update the page number on the pagination element. + if (!isLastPage) { + $(".c-pagination button").attr("data-page", currentPage + 1); + } + }; + + /** + * Parse a form instance into an object. + * + * @param {HTMLElement} form + * @returns {[string, string][]} + */ + + const getFormData = (form) => { + const formData = new FormData(form); + + const prefix = formData.get("prefix"); + + // Loop all form entries, remove the prefix from keys, and assign to data. + const entries = [...formData.entries()] + .map(([key, value]) => { + // Remove prefix if it exists. + const newKey = key.startsWith(prefix) ? key.replace(prefix, "") : key; + + // Parse the page number to integer. + if ("page" === newKey) { + value = parseInt(value); + } + + return [newKey, value]; + }) + .filter(([key, value]) => { + // Skip prefix and template keys, and empty values. + if (["prefix"].includes(key) || value === "") { + return false; + } + + if (key === "post_type" && value === "posts") { + console.error("posts needs to transformed to post! or edit the form"); + value = "post"; + } + + return true; + }); + + entries.push(["action", $(form).attr("action")]); + + return Object.fromEntries(entries); + }; + + $.fn.moji_ajaxFilter = function () { + const DEFAULT_AJAX_PROPS = { + type: "POST", + url: mojAjax.ajaxurl, + dataType: "json", + success: (response) => { + renderResults(response); + renderPagination(response.aggregates); + }, + }; + + const $form = $("#ff, #ff_events"); + + const initialFormData = getFormData($form.get(0)); + + // On page load get the ajax props and store them on the pagination element. + $(".c-pagination").data("ajax-props", { + ...DEFAULT_AJAX_PROPS, + data: initialFormData, + }); + + /** + * Handle a form submit event. + * + * @param {Event} e + * @returns {void} + */ + + $form.on("submit", function (e) { + e.preventDefault(); + + const ajaxProps = { + ...DEFAULT_AJAX_PROPS, + data: getFormData(this), + }; + + // Make the ajax request. + $.ajax(ajaxProps); + + // Store the ajax props on the pagination element. + $(".c-pagination").data("ajax-props", ajaxProps); + }); + + /** + * Handle events on the pagination button. + * + * @param {Event} e + * @returns {void} + */ + + $(".c-pagination").on("click keydown", "button", function (e) { + e.stopPropagation(); + + // If the event is a keydown event, only allow enter or space keys. + if (e.type === "keydown" && ![13, 32].includes(e.keyCode)) { + return; + } + + // Get the ajax props from the pagination element. + const ajaxProps = $(this).closest(".c-pagination").data("ajax-props"); + + // Get the page number from the button. + const page = $(this).data("page"); + + // Make an ajax request with the new page number. + $.ajax({ ...ajaxProps, data: { ...ajaxProps.data, page } }); + }); + }; +})(jQuery); diff --git a/public/app/themes/clarity/src/globals/js/ajax-templating.js b/public/app/themes/clarity/src/globals/js/ajax-templating.js new file mode 100644 index 000000000..38d039aaf --- /dev/null +++ b/public/app/themes/clarity/src/globals/js/ajax-templating.js @@ -0,0 +1,87 @@ +/** + * This Class is responsible for rendering HTML from AJAX template. + * + * @see https://stackoverflow.com/a/39065147/6671505 + * + * @example + * const template = new AjaxTemplating("results-template"); + * const html = template.renderHtml({ + * title: "Hello World", + * content: "This is a test." + * }); + */ + +export default class AjaxTemplating { + /** + * Constructor + * + * @param {string} templateName + */ + constructor(templateName) { + this.resultsTemplate = this.loadTemplate(templateName); + } + + /** + * Load the template from the DOM. + * + * Returns an array of strings where: + * - every odd index is a template variable + * - every even is a string of html text + * + * @param {string} templateName + * @returns {string[]} + */ + + loadTemplate(templateName) { + // do this without jQuery + return document + .querySelector(`script[data-template="${templateName}"]`) + .textContent.split(/\$\{(.+?)\}/g); + } + + /** + * Render the HTML from the template and props. + * + * @param {Object} props + * @returns {string} + */ + + renderHtml(props) { + // Keep track of the conditional blocks + // If a conditional block is not met, we skip the block + let skip = false; + + let parts = []; + + for (let i = 0; i < this.resultsTemplate.length; i++) { + const tok = this.resultsTemplate[i]; + + // Handle the html text - even indexes + + if (i % 2 === 0 && !skip) { + parts.push(tok); + continue; + } + + if (i % 2 === 0) { + continue; + } + + // Handle the template variables - odd indexes + + if (tok.startsWith("?") && !props[tok.substring(1)]) { + skip = true; + } + + if (tok.startsWith("/?") && !props[tok.substring(2)]) { + skip = false; + } + + if (!skip) { + parts.push(props[tok]); + } + } + + return parts.join(""); + } +} diff --git a/public/app/themes/clarity/src/globals/js/blog-content_filter.js b/public/app/themes/clarity/src/globals/js/blog-content_filter.js deleted file mode 100644 index b187d0925..000000000 --- a/public/app/themes/clarity/src/globals/js/blog-content_filter.js +++ /dev/null @@ -1,227 +0,0 @@ -export default (function ($) { - /** - * A helper to render a template with props. - * - * @see https://stackoverflow.com/a/39065147/6671505 - * - * @param {[string, string|number]} props - * @returns {function(string, number): string} - */ - const templateRender = (props) => (tok, i) => i % 2 ? props[tok] : tok; - - /** - * Render the response to the page. - * - * The results type should match the php response object - * as returned by the `FilterSearch->mapResults()` method. - * @typedef {Object} Result - * @property {string} ID - * @property {string} post_title - * @property {string} post_date_formatted - * @property {string} post_excerpt_formatted - * @property {string} permalink - * @property {string} post_type - * @property {string} post_thumbnail - * @property {string} post_thumbnail_alt - * - * The response type should match the php response object - * as returned by the `FilterSearch->loadSearchResults()` method. - * @typedef {Object} Response - * @property {Object} aggregates - * @property {number} aggregates.currentPage - * @property {number} aggregates.resultsPerPage - * @property {number} aggregates.totalResults - * @property {Result[]} results - * - * Templates used for rendering the response. - * @typedef {Object} Templates - * @property {string[]} result - * @property {string[]} pagination - * - * @param {Response} response - * @param {Templates} templates - */ - - const renderResponse = ( - { results, aggregates: { currentPage, resultsPerPage, totalResults } }, - templates, - ) => { - // Remove all articles if page is 1. - if (currentPage === 1) { - $(".c-article-item").remove(); - } - - // Build the html from the response. - // See https://stackoverflow.com/a/39065147/6671505 - const resultsHtml = results.map((props) => - templates.result.map(templateRender(props)).join(""), - ); - - // Append the html to the content section. - $("#content").append(resultsHtml); - - // Update the title and pagination. - $("#title-section").text(`${totalResults} search results`); - - const isLastPage = currentPage * resultsPerPage >= totalResults; - - const paginationTitle = ({ totalResults, isLastPage }) => { - if (!totalResults) { - return "No Results"; - } - if (isLastPage) { - return "No More Results"; - } - return `Load Next ${resultsPerPage} Results`; - }; - - // Update the pagination. - const paginationHtml = templates.pagination - .map( - templateRender({ - title: paginationTitle({ totalResults, isLastPage }), - // Disable the button if it's the last page. - disabled: isLastPage ? `disabled="disabled"` : "", - // Adjust the zero-indexed current page. - currentPageFormatted: parseInt(currentPage), - // Calculate the total pages - if no results, then set as 1 to render '1 of 1'. - totalPages: !totalResults - ? 1 - : Math.ceil(totalResults / resultsPerPage), - }), - ) - .join(""); - - $(".c-pagination").html(paginationHtml); - }; - - const getAjaxProps = (form) => { - const formData = new FormData(form); - - const prefix = formData.get("prefix"); - - const resultTemplateName = formData.get("template"); - const templates = { - result: $(`script[data-template="${resultTemplateName}"]`) - .text() - .split(/\$\{(.+?)\}/g), - pagination: $(`script[data-template="pagination"]`) - .text() - .split(/\$\{(.+?)\}/g), - }; - - // Loop all form entries, remove the prefix from keys, and assign to data. - const entries = [...formData.entries()] - .map(([key, value]) => { - // Remove prefix if it exists. - const newKey = key.startsWith(prefix) ? key.replace(prefix, "") : key; - - // Parse the page number to integer. - if ("page" === newKey) { - value = parseInt(value); - } - - return [newKey, value]; - }) - .filter(([key, value]) => { - // Skip prefix and template keys. - if (["prefix", "template"].includes(key)) { - return false; - } - - // Skip empty values. - if (value === "") { - return false; - } - - if (key === "post_type" && value === "posts") { - console.error("posts needs to transformed to post! or edit the form"); - value = "post"; - } - - return true; - }); - - entries.push(["action", $(form).attr("action")]); - - const data = Object.fromEntries(entries); - - const ajaxProps = { - type: $(form).attr("method"), - url: mojAjax.ajaxurl, - dataType: "json", - data, - success: (response) => renderResponse(response, templates), - }; - - return ajaxProps; - }; - - $.fn.moji_ajaxFilter = function () { - - // On page load get the form data and store it on the pagination element. - const ajaxProps = getAjaxProps($("#ff").get(0)); - // Store form data on the pagination element. - $(".c-pagination").data("ajax-props", ajaxProps); - - $("#ff").on("submit", function (e) { - e.preventDefault(); - - const ajaxProps = getAjaxProps(this); - - // Store form data on the pagination element. - $(".c-pagination").data("ajax-props", ajaxProps); - - console.log($(".c-pagination").data("parent-form")); - - $.ajax(ajaxProps); - }); - - $(".c-pagination").on("click keydown", "button", function (e) { - e.stopPropagation(); - - if (e.type === "keydown" && ![13, 32].includes(e.keyCode)) { - console.log("pressed: ".e.keyCode); - return; - } - - const ajaxProps = $(this).closest(".c-pagination").data("ajax-props"); - - // if(!ajaxProps) { - // // handle the load next button being pressed without a filter. - // $("#ff").submit(); - // return - // } - - ajaxProps.data.page += 1; - - $.ajax(ajaxProps); - }); - - $(document).on("submit", "#ff_events", function (e) { - e.preventDefault(); - - const nextPageToRetrieve = $("#ff").data("page") + 1; - $(".more-btn").attr("data-page", nextPageToRetrieve); - - $.ajax({ - type: "post", - url: mojAjax.ajaxurl, - dataType: "html", - data: { - action: "load_events_filter_results", - query: $(this).find('input[name="ff_keywords_filter"]').val(), - valueSelected: $(this).find("#ff_date_filter option:selected").val(), - postType, - termID, - nonce_hash: nonceHash, - }, - success: function (response) { - $(".c-article-item").remove(); - $("#content").html(response); - }, - }); - return false; - }); - }; -})(jQuery); diff --git a/public/app/themes/clarity/src/globals/js/script-loader.js b/public/app/themes/clarity/src/globals/js/script-loader.js index fe8e38070..741a721c0 100644 --- a/public/app/themes/clarity/src/globals/js/script-loader.js +++ b/public/app/themes/clarity/src/globals/js/script-loader.js @@ -15,7 +15,7 @@ import "../../components/c-notes-from-antonia/lazy_load.js"; // Global scripts import "../../../inc/admin/js/feedback.js"; import "./auth-heartbeat.js"; -import "./blog-content_filter.js"; +import "./ajax-filter.js"; import "./condolences-filter.js"; import "./equaliser.js"; import "./slider.js";