array( 'effect' => 'fade', 'fadeEffect' => array( 'crossFade' => true ), 'slidesPerView' => 1, 'spaceBetween' => 0, 'breakpoints' => null, ), 'cards' => array( 'effect' => 'slide', 'grabCursor' => true, ), 'grid' => array( 'effect' => 'slide', 'slidesPerView' => 1, 'spaceBetween' => 24, 'grabCursor' => true, 'breakpoints' => null, ), 'featured' => array( 'effect' => 'fade', 'fadeEffect' => array( 'crossFade' => true ), 'slidesPerView' => 1, 'spaceBetween' => 0, 'breakpoints' => null, ), 'timeline' => array( 'effect' => 'slide', 'grabCursor' => true, ), ); /** All designs ('' = the original slider). */ const DESIGNS = array( 'magazine', 'cards', 'grid', 'featured', 'timeline' ); /** Option holding the cache version; bumped whenever posts change. */ const CACHE_VERSION_OPTION = 'gutslider_post_slider_cache_v'; /** Fallback badge colours, picked by term id (same list as the editor). */ const BADGE_PALETTE = array( '#2563eb', '#db2777', '#059669', '#d97706', '#7c3aed', '#dc2626', '#0891b2', '#4f46e5' ); /** * Constructor. */ public function __construct() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); // Anything that can change what a slider shows invalidates cached results. foreach ( array( 'save_post', 'deleted_post', 'trashed_post', 'untrashed_post', 'set_object_terms', 'edited_term', 'delete_term', 'wp_update_comment_count', 'update_option_sticky_posts' ) as $hook ) { add_action( $hook, array( __CLASS__, 'flush_cache' ) ); } add_action( 'transition_post_status', array( $this, 'on_status_change' ), 10, 3 ); add_action( 'updated_post_meta', array( $this, 'on_meta_change' ), 10, 3 ); add_action( 'added_post_meta', array( $this, 'on_meta_change' ), 10, 3 ); add_action( 'deleted_post_meta', array( $this, 'on_meta_change' ), 10, 3 ); } /* --------------------------------------------------------------------- * Attributes * ------------------------------------------------------------------ */ /** * Attributes with defaults; scalars sanitised as text (booleans become '1' / ''). * * @param array $attributes Block attributes. * @return array */ public static function normalize( $attributes ) { $defaults = array( 'showTitle' => true, 'showCat' => true, 'showExcerpt' => true, 'showBtn' => true, 'linkTitle' => true, 'titleTag' => 'h2', 'btnLabel' => __( 'Read More', 'slider-blocks' ), 'enableOverlay' => true, 'sliderType' => 'slider', 'uniqueId' => '', 'navContainerPosition' => 'nav_inside', 'navPosition' => 'nav_cc', 'catAnimation' => '', 'titleAnimation' => '', 'exptAnimation' => '', 'btnAnimation' => '', 'showNavigation' => true, 'showPagination' => true, 'customNavIcon' => false, 'navIconSource' => 'library', 'navPrevIcon' => 'arrowLeft', 'navNextIcon' => 'arrowRight', 'customPrevSVG' => '', 'customNextSVG' => '', 'excerptLength' => 20, 'enableRemoteNav' => false, 'remotePrevSelector' => '', 'remoteNextSelector' => '', 'detachContent' => false, 'showThumbs' => false, 'thumbContent' => 'image', 'thumbTitlePosition' => 'below', 'hideWhenEmpty' => true, 'emptyMessage' => '', 'showDate' => false, 'showAuthor' => false, 'showAvatar' => false, 'showReadingTime' => false, 'showComments' => false, 'cardLink' => false, 'imageSize' => 'full', 'preset' => '', 'catStyle' => '', 'catPosition' => 'above-title', 'highlightCurrent' => false, 'schemaMarkup' => false, 'loadMore' => false, 'loadMoreMax' => 30, 'infiniteLoop' => true, ); $atts = wp_parse_args( $attributes, $defaults ); foreach ( $atts as $key => $value ) { if ( is_scalar( $value ) || null === $value ) { $atts[ $key ] = sanitize_text_field( (string) $value ); } } $atts['sliderOptions'] = isset( $attributes['sliderOptions'] ) ? $attributes['sliderOptions'] : array(); $atts['thumbsOptions'] = isset( $attributes['thumbsOptions'] ) ? $attributes['thumbsOptions'] : array(); $atts['focusPoints'] = isset( $attributes['focusPoints'] ) && is_array( $attributes['focusPoints'] ) ? $attributes['focusPoints'] : array(); $atts['preset'] = in_array( $atts['preset'], self::DESIGNS, true ) ? $atts['preset'] : ''; return $atts; } /** * Post type to query: any public post type, 'post' otherwise. * * @param array $attributes Block attributes. * @return string */ public static function post_type( $attributes ) { $type = isset( $attributes['postType'] ) ? sanitize_key( $attributes['postType'] ) : 'post'; return ( $type && post_type_exists( $type ) && is_post_type_viewable( $type ) && 'attachment' !== $type ) ? $type : 'post'; } /** * Taxonomies of a post type that can filter or label slides. * * @param string $post_type Post type. * @return string[] */ public static function taxonomies_for( $post_type ) { $names = array(); foreach ( get_object_taxonomies( $post_type, 'objects' ) as $taxonomy ) { if ( $taxonomy->public && $taxonomy->show_ui && 'post_format' !== $taxonomy->name ) { $names[] = $taxonomy->name; } } return $names; } /** * Taxonomies whose terms label a slide: the ones the slider is filtered by, else the * post type's categories (core `category`, or its first hierarchical taxonomy). * * @param array $attributes Block attributes. * @return string[] */ public static function label_taxonomies( $attributes ) { $query_type = isset( $attributes['queryType'] ) ? sanitize_key( $attributes['queryType'] ) : 'latest_posts'; if ( 'specific_categories' === $query_type && ! empty( $attributes['taxQuery'] ) && is_array( $attributes['taxQuery'] ) ) { return array_map( 'sanitize_key', array_keys( $attributes['taxQuery'] ) ); } $post_type = self::post_type( $attributes ); $taxonomies = self::taxonomies_for( $post_type ); // The post type's own categories first (e.g. product_cat), then any hierarchical one. foreach ( array( 'category', $post_type . '_cat', $post_type . '_category' ) as $preferred ) { if ( in_array( $preferred, $taxonomies, true ) ) { return array( $preferred ); } } foreach ( $taxonomies as $taxonomy ) { if ( is_taxonomy_hierarchical( $taxonomy ) ) { return array( $taxonomy ); } } return $taxonomies ? array( $taxonomies[0] ) : array(); } /* --------------------------------------------------------------------- * Query * ------------------------------------------------------------------ */ /** * WP_Query arguments from the block's saved query settings. * * @param array $attributes Block attributes. * @param int $current Post the slider is shown on (0 if none). * @return array */ public static function query_args( $attributes, $current = 0 ) { $query_type = isset( $attributes['queryType'] ) ? sanitize_key( $attributes['queryType'] ) : 'latest_posts'; $post_type = self::post_type( $attributes ); $total = isset( $attributes['totalPosts'] ) ? (int) $attributes['totalPosts'] : 3; $orderby = isset( $attributes['postOrderBy'] ) ? sanitize_key( $attributes['postOrderBy'] ) : 'date'; $order = isset( $attributes['postOrder'] ) && 'asc' === strtolower( (string) $attributes['postOrder'] ) ? 'ASC' : 'DESC'; $args = array( 'post_type' => $post_type, 'post_status' => 'publish', // An empty "Total Posts" meant every post; cap it like the REST API does. 'posts_per_page' => $total > 0 ? min( $total, 100 ) : 100, 'orderby' => in_array( $orderby, array( 'date', 'title', 'modified', 'comment_count', 'rand' ), true ) ? $orderby : 'date', 'order' => $order, 'ignore_sticky_posts' => true, 'no_found_rows' => true, ); $is_specific = 'specific_posts' === $query_type && ! empty( $attributes['specificPosts'] ) && is_array( $attributes['specificPosts'] ); $not_in = array(); // Skip the first N posts (not for hand-picked posts). $offset = isset( $attributes['postOffset'] ) ? absint( $attributes['postOffset'] ) : 0; if ( $offset && ! $is_specific ) { $args['offset'] = $offset; } // Leave out the post this slider is shown on (always, in Related mode). if ( $current && ( ! empty( $attributes['excludeCurrent'] ) || 'related' === $query_type ) ) { $not_in[] = (int) $current; } // Sticky posts: normal place (default), first (see self::query_ids()), or hidden. if ( ! $is_specific && isset( $attributes['stickyPosts'] ) && 'exclude' === $attributes['stickyPosts'] ) { $not_in = array_merge( $not_in, array_map( 'absint', (array) get_option( 'sticky_posts', array() ) ) ); } if ( $not_in ) { $args['post__not_in'] = array_values( array_unique( $not_in ) ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in } if ( ! empty( $attributes['onlyWithImage'] ) ) { $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => '_thumbnail_id', 'compare' => 'EXISTS', ), ); } $tax_query = array(); if ( $is_specific ) { $ids = array_filter( array_map( 'absint', wp_list_pluck( $attributes['specificPosts'], 'value' ) ) ); if ( $ids ) { $args['post__in'] = $ids; $args['posts_per_page'] = count( $ids ); } } if ( 'specific_categories' === $query_type ) { // `taxQuery` ({ taxonomy: [{ label, value }] }) covers every taxonomy; // `specificCategories` is the legacy core-category selection. $selected = isset( $attributes['taxQuery'] ) && is_array( $attributes['taxQuery'] ) && ! empty( $attributes['taxQuery'] ) ? $attributes['taxQuery'] : ( ! empty( $attributes['specificCategories'] ) && is_array( $attributes['specificCategories'] ) ? array( 'category' => $attributes['specificCategories'] ) : array() ); foreach ( $selected as $taxonomy => $terms ) { $taxonomy = sanitize_key( $taxonomy ); $term_ids = is_array( $terms ) ? array_filter( array_map( 'absint', wp_list_pluck( $terms, 'value' ) ) ) : array(); if ( $term_ids && taxonomy_exists( $taxonomy ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids, ); } } } // Related: posts sharing any of the current post's terms. if ( 'related' === $query_type && $current ) { $related = array(); foreach ( self::related_taxonomies( $attributes, $post_type ) as $taxonomy ) { $term_ids = wp_get_post_terms( $current, $taxonomy, array( 'fields' => 'ids' ) ); if ( $term_ids && ! is_wp_error( $term_ids ) ) { $related[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids, ); } } if ( $related ) { $tax_query[] = array_merge( array( 'relation' => 'OR' ), $related ); } } // Leave out posts in these terms, for any query but hand-picked posts. if ( ! $is_specific && ! empty( $attributes['excludeTerms'] ) && is_array( $attributes['excludeTerms'] ) ) { foreach ( $attributes['excludeTerms'] as $taxonomy => $terms ) { $taxonomy = sanitize_key( $taxonomy ); $term_ids = is_array( $terms ) ? array_filter( array_map( 'absint', wp_list_pluck( $terms, 'value' ) ) ) : array(); if ( $term_ids && taxonomy_exists( $taxonomy ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids, 'operator' => 'NOT IN', ); } } } if ( $tax_query ) { // Every group must match (same as the REST query the editor previews with). $args['tax_query'] = array_merge( array( 'relation' => 'AND' ), $tax_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } /** * Filters the query behind a Blog Post slider. * * @param array $args WP_Query arguments. * @param array $attributes Block attributes. */ return apply_filters( 'gutslider_post_slider_query_args', $args, $attributes ); } /** * Taxonomies Related mode matches on. * * @param array $attributes Block attributes. * @param string $post_type Post type. * @return string[] */ public static function related_taxonomies( $attributes, $post_type ) { $available = self::taxonomies_for( $post_type ); if ( 'post' === $post_type ) { $by = isset( $attributes['relatedBy'] ) ? sanitize_key( $attributes['relatedBy'] ) : 'both'; $wanted = 'category' === $by ? array( 'category' ) : ( 'post_tag' === $by ? array( 'post_tag' ) : array( 'category', 'post_tag' ) ); return array_values( array_intersect( $wanted, $available ) ); } return $available; } /** * Post ids for a page of the slider (page 0 = what the page shows first), * cached until any post, term, comment or sticky post changes. * * @param array $attributes Block attributes. * @param int $current Post the slider is shown on. * @param int $page Page (for "Load more"). * @return array { ids: int[], more: bool } */ public static function query_ids( $attributes, $current = 0, $page = 0 ) { $args = self::query_args( $attributes, $current ); $per_page = (int) $args['posts_per_page']; $offset = isset( $args['offset'] ) ? (int) $args['offset'] : 0; $sticky = isset( $attributes['stickyPosts'] ) && 'first' === $attributes['stickyPosts'] && empty( $args['post__in'] ); $max = ! empty( $attributes['loadMore'] ) ? max( $per_page, min( 100, absint( $attributes['loadMoreMax'] ?? 30 ) ) ) : $per_page; // Hand-picked posts have no further pages. if ( $page > 0 && ! empty( $args['post__in'] ) && ! $sticky ) { return array( 'ids' => array(), 'more' => false ); } $use_cache = 'rand' !== $args['orderby'] && apply_filters( 'gutslider_post_slider_use_cache', true, $attributes ); $cache_key = 'gs_ps_' . md5( wp_json_encode( array( $args, $sticky, $page, $max, self::cache_version() ) ) ); if ( $use_cache ) { $cached = get_transient( $cache_key ); if ( is_array( $cached ) && isset( $cached['ids'] ) ) { return $cached; } } $start = $offset + $page * $per_page; $want = $per_page + 1; // One extra tells whether there is more. if ( $sticky ) { $sticky_ids = array_filter( array_map( 'absint', (array) get_option( 'sticky_posts', array() ) ) ); $lookup = array_merge( $args, array( 'fields' => 'ids', 'offset' => 0, 'posts_per_page' => $start + $want, 'suppress_filters' => false ) ); $stickies = $sticky_ids ? get_posts( array_merge( $lookup, array( 'post__in' => $sticky_ids ) ) ) : array(); $others = get_posts( array_merge( $lookup, array( 'post__not_in' => array_merge( $args['post__not_in'] ?? array(), $sticky_ids ) ) ) ); $ids = array_slice( array_merge( $stickies, $others ), $start, $want ); } else { $query = new \WP_Query( array_merge( $args, array( 'fields' => 'ids', 'offset' => $start, 'posts_per_page' => $want ) ) ); $ids = array_map( 'intval', $query->posts ); } $more = count( $ids ) > $per_page; $ids = array_slice( $ids, 0, $per_page ); // "Load more" stops at the chosen maximum. $shown_after = ( $page + 1 ) * $per_page; if ( $shown_after >= $max ) { $ids = array_slice( $ids, 0, max( 0, $max - $page * $per_page ) ); $more = false; } $result = array( 'ids' => array_map( 'intval', $ids ), 'more' => $more && ! empty( $attributes['loadMore'] ), ); if ( $use_cache ) { set_transient( $cache_key, $result, (int) apply_filters( 'gutslider_post_slider_cache_ttl', 12 * HOUR_IN_SECONDS ) ); } return $result; } /** * Everything a slide needs, for the given post ids (in that order). * * @param int[] $ids Post ids. * @param array $attributes Block attributes. * @param int $current Post the slider is shown on. * @return array[] */ public static function items( $ids, $attributes, $current = 0 ) { if ( ! $ids ) { return array(); } $taxonomies = self::label_taxonomies( $attributes ); $excerpt_length = isset( $attributes['excerptLength'] ) ? (int) $attributes['excerptLength'] : 20; // Plain text only, but keep a leading space (" Read more"). $excerpt_ending = isset( $attributes['excerptEnding'] ) ? wp_kses( (string) $attributes['excerptEnding'], array() ) : '...'; $query = new \WP_Query( array( 'post_type' => 'any', 'post_status' => 'publish', 'post__in' => $ids, 'orderby' => 'post__in', 'posts_per_page' => count( $ids ), 'ignore_sticky_posts' => true, 'no_found_rows' => true, ) ); $items = array(); while ( $query->have_posts() ) { $query->the_post(); $post_id = get_the_ID(); $terms = array(); foreach ( $taxonomies as $taxonomy ) { $post_terms = get_the_terms( $post_id, $taxonomy ); if ( $post_terms && ! is_wp_error( $post_terms ) ) { $terms = array_merge( $terms, $post_terms ); } } // Excerpt: the post's own excerpt, or its opening words when it has none, // cut to the chosen length with the chosen ending. Protected posts keep it private. if ( post_password_required() ) { $excerpt = ''; } elseif ( $excerpt_length > 0 ) { $source = has_excerpt() ? get_the_excerpt() : wp_strip_all_tags( excerpt_remove_blocks( strip_shortcodes( get_the_content() ) ) ); $excerpt = wp_trim_words( $source, $excerpt_length, $excerpt_ending ); } else { $excerpt = apply_filters( 'the_excerpt', get_the_excerpt() ); } $author_id = (int) get_post_field( 'post_author', $post_id ); $words = count( preg_split( '/\s+/u', trim( wp_strip_all_tags( strip_shortcodes( get_the_content() ) ) ), -1, PREG_SPLIT_NO_EMPTY ) ); $items[] = array( 'id' => $post_id, 'type' => get_post_type(), 'title' => get_the_title(), 'excerpt' => $excerpt, 'link' => get_permalink(), 'image_id' => (int) get_post_thumbnail_id(), 'terms' => $terms, 'date' => get_the_date(), 'date_iso' => get_the_date( 'c' ), 'modified' => get_the_modified_date( 'c' ), 'author' => $author_id ? get_the_author_meta( 'display_name', $author_id ) : '', 'author_id' => $author_id, // About 200 words a minute; at least one minute. 'reading' => max( 1, (int) ceil( $words / 200 ) ), 'comments' => (int) get_comments_number(), 'is_current' => $current && $post_id === (int) $current, ); } wp_reset_postdata(); return $items; } /** * Whether the slide background was chosen by the user (not the untouched #efefef default). * Featured + List and Timeline only show a chosen one, as a card. Keep in sync with * hasCustomSlideBg() in presets.js. * * @param array $attributes Block attributes. * @return bool */ public static function has_custom_slide_bg( $attributes ) { if ( isset( $attributes['bgType'] ) && 'gradient' === $attributes['bgType'] ) { return ! empty( $attributes['bgGradient'] ); } if ( ! empty( $attributes['bgImage']['url'] ) ) { return true; } $color = isset( $attributes['bgColor'] ) ? strtolower( (string) $attributes['bgColor'] ) : ''; return '' !== $color && '#efefef' !== $color; } /** * Badge colour of a term: its own (set on the term screen), else one from the palette. * * @param \WP_Term $term Term. * @return string Hex colour. */ public static function term_color( $term ) { $color = sanitize_hex_color( (string) get_term_meta( $term->term_id, 'gutslider_color', true ) ); return $color ? $color : self::BADGE_PALETTE[ $term->term_id % count( self::BADGE_PALETTE ) ]; } /* --------------------------------------------------------------------- * Markup * ------------------------------------------------------------------ */ /** * Post the slider is shown on (the post whose content or template holds it). * * @return int */ public static function current_post_id() { $id = get_the_ID(); if ( ! $id && is_singular() ) { $id = get_queried_object_id(); } return (int) $id; } /** * Whole block. * * @param array $attributes Block attributes. * @return string HTML. */ public static function render( $attributes ) { $atts = self::normalize( $attributes ); $current = self::current_post_id(); $result = self::query_ids( $attributes, $current ); $items = self::items( $result['ids'], $attributes, $current ); // Nothing matched: hide the block (default), or say so. if ( empty( $items ) ) { if ( $atts['hideWhenEmpty'] ) { return ''; } return sprintf( '

%2$s

', get_block_wrapper_attributes( array( 'class' => esc_attr( $atts['uniqueId'] ) ) ), esc_html( '' !== $atts['emptyMessage'] ? $atts['emptyMessage'] : __( 'No posts found.', 'slider-blocks' ) ) ); } $show_thumbs = '1' === $atts['showThumbs'] && ! in_array( $atts['preset'], self::NO_THUMBS_DESIGNS, true ); // "Load more" needs the block in this post's own content (the endpoint looks it up // there) and no infinite loop. $load_more = $result['more'] && $atts['uniqueId'] && $current && ! $atts['infiniteLoop'] && ! in_array( $atts['preset'], self::NO_LOAD_MORE_DESIGNS, true ) && false !== strpos( (string) get_post_field( 'post_content', $current ), '"uniqueId":"' . $atts['uniqueId'] . '"' ); $slider_options = $atts['sliderOptions']; if ( is_array( $slider_options ) && isset( self::PRESET_SWIPER_OPTIONS[ $atts['preset'] ] ) ) { foreach ( self::PRESET_SWIPER_OPTIONS[ $atts['preset'] ] as $key => $value ) { if ( null === $value ) { unset( $slider_options[ $key ] ); } else { $slider_options[ $key ] = $value; } } } $block_classes = array( esc_attr( $atts['uniqueId'] ), esc_attr( $atts['navContainerPosition'] ), esc_attr( $atts['navPosition'] ), $atts['preset'] ? 'gutslider-preset-' . $atts['preset'] : '', 'badge' === $atts['catStyle'] ? 'has-category-badges' : '', in_array( $atts['preset'], array( 'featured', 'timeline' ), true ) && self::has_custom_slide_bg( $attributes ) ? 'has-slide-bg' : '', ); $block_props = get_block_wrapper_attributes( array( 'class' => implode( ' ', array_filter( $block_classes ) ) ) ); ob_start(); ?>
data-swiper-options="" >
'; } ?>
'; } ?> $width <= $cap_width, ARRAY_FILTER_USE_KEY ) : $sources; }; add_filter( 'wp_calculate_image_srcset', $cap_srcset ); } $html = ''; if ( 'grid' !== $atts['preset'] ) { foreach ( $items as $i => $item ) { $html .= self::render_slide( $item, $atts, $start + $i + 1, $image_size, $image_sizes ); } } else { // Overlay Grid slides whole pages of tiles. foreach ( array_chunk( $items, self::GRID_PAGE_SIZE ) as $p => $page ) { $html .= '
'; foreach ( $page as $i => $item ) { $html .= self::render_slide( $item, $atts, $start + $p * self::GRID_PAGE_SIZE + $i + 1, $image_size, $image_sizes ); } $html .= '
'; } } if ( $cap_srcset ) { remove_filter( 'wp_calculate_image_srcset', $cap_srcset ); } return $html; } /** * One slide. * * @param array $item Slide data. * @param array $atts Normalised attributes. * @param int $index 1-based position. * @param string $image_size Image size. * @param string $image_sizes `sizes` attribute. * @return string HTML. */ private static function render_slide( $item, $atts, $index, $image_size, $image_sizes ) { $post_title = $item['title']; $post_link = $item['link']; $post_image_id = $item['image_id']; $post_image = $post_image_id ? wp_get_attachment_image_url( $post_image_id, $image_size ) : ''; // The first slide is likely the largest thing on screen: load it right away. $image_loading = 1 === $index ? 'eager' : 'lazy'; $has_meta = $atts['showDate'] || $atts['showAuthor'] || $atts['showReadingTime'] || $atts['showComments']; $is_current = $item['is_current'] && '1' === $atts['highlightCurrent']; $badges = 'badge' === $atts['catStyle']; // Badges sit on the image only when there is one; otherwise above the title. $cats_on_image = $badges && $post_image && ! $atts['detachContent'] && in_array( $atts['catPosition'], array( 'top-left', 'top-right' ), true ); $focus = $atts['focusPoints'][ $index - 1 ] ?? null; $object_position = isset( $focus['x'], $focus['y'] ) ? sprintf( 'object-position: %d%% %d%%;', round( $focus['x'] * 100 ), round( $focus['y'] * 100 ) ) : 'object-position: center center;'; $classes = array( 'swiper-slide' ); if ( 'slider' === $atts['sliderType'] ) { $classes[] = 'slide-mode'; } if ( ! empty( $atts['detachContent'] ) ) { $classes[] = 'detach-content'; } if ( ! $post_image && $atts['preset'] ) { $classes[] = 'no-image'; } if ( $is_current ) { $classes[] = 'is-current'; } $categories = ''; if ( $atts['showCat'] && ! empty( $item['terms'] ) ) { $categories .= '
'; foreach ( $item['terms'] as $term ) { if ( $badges ) { $categories .= sprintf( '%2$s', esc_attr( self::term_color( $term ) ), esc_html( $term->name ) ); } else { $categories .= sprintf( '%s', esc_html( $term->name ) ); } } $categories .= '
'; } ob_start(); ?>
>
'gutslider-post-bg', // Decorative: the title right on top already names the slide. 'alt' => '', 'sizes' => $image_sizes, 'loading' => $image_loading, 'decoding' => 'async', 'style' => $object_position, ) ); } ?>
$image_alt ? $image_alt : wp_strip_all_tags( $post_title ), 'sizes' => $image_sizes, 'loading' => $image_loading, 'decoding' => 'async', 'style' => $object_position, ) ); ?>
%3$s', $title_tag, esc_attr( $atts['titleAnimation'] ), esc_html( $post_title ) ); if ( $atts['linkTitle'] ) { printf( '%s', esc_url( $post_link ), $title_content // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } else { echo wp_kses_post( $title_content ); } } ?>
">
$item ) : $is_current = $item['is_current'] && '1' === $atts['highlightCurrent']; ?>
<?php echo esc_attr( $thumb_alt ); ?>
element. */ public static function schema( $items ) { $list = array(); foreach ( $items as $position => $item ) { $entry = array( '@type' => 'post' === $item['type'] ? 'BlogPosting' : ( 'page' === $item['type'] ? 'WebPage' : 'CreativeWork' ), 'headline' => wp_strip_all_tags( $item['title'] ), 'url' => $item['link'], 'datePublished' => $item['date_iso'], 'dateModified' => $item['modified'], ); if ( $item['author'] ) { $entry['author'] = array( '@type' => 'Person', 'name' => $item['author'], ); } if ( $item['image_id'] ) { $image = wp_get_attachment_image_url( $item['image_id'], 'large' ); if ( $image ) { $entry['image'] = $image; } } $list[] = array( '@type' => 'ListItem', 'position' => $position + 1, 'item' => $entry, ); } $data = array( '@context' => 'https://schema.org', '@type' => 'ItemList', 'itemListElement' => $list, ); return ''; } /* --------------------------------------------------------------------- * "Load more" endpoint * ------------------------------------------------------------------ */ /** * GET gutslider/v1/post-slider/more?post=ID&block=UNIQUE_ID&page=N * * Public and read only. The block's settings are read from the post it was saved * in, never taken from the request, and only published, unprotected posts count. */ public function register_routes() { register_rest_route( 'gutslider/v1', '/post-slider/more', array( 'methods' => 'GET', 'callback' => array( $this, 'more' ), 'permission_callback' => '__return_true', 'args' => array( 'post' => array( 'type' => 'integer', 'required' => true, 'minimum' => 1, ), 'block' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_html_class', ), 'page' => array( 'type' => 'integer', 'required' => true, 'minimum' => 1, 'maximum' => 50, ), ), ) ); } /** * Next slides. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function more( $request ) { $post = get_post( (int) $request['post'] ); if ( ! $post || 'publish' !== $post->post_status || post_password_required( $post ) || ! is_post_type_viewable( $post->post_type ) ) { return new \WP_Error( 'gutslider_not_found', __( 'Not found.', 'slider-blocks' ), array( 'status' => 404 ) ); } $attributes = self::find_block( parse_blocks( $post->post_content ), (string) $request['block'] ); if ( ! $attributes || empty( $attributes['loadMore'] ) ) { return new \WP_Error( 'gutslider_not_found', __( 'Not found.', 'slider-blocks' ), array( 'status' => 404 ) ); } // Render as if on that post (related posts, "now reading", excluded current post). $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited setup_postdata( $post ); $page = (int) $request['page']; $result = self::query_ids( $attributes, $post->ID, $page ); $items = self::items( $result['ids'], $attributes, $post->ID ); $atts = self::normalize( $attributes ); $per = max( 1, (int) self::query_args( $attributes, $post->ID )['posts_per_page'] ); $html = self::render_slides( $items, $atts, $page * $per ); wp_reset_postdata(); return rest_ensure_response( array( 'html' => $html, 'count' => count( $items ), 'more' => $result['more'], ) ); } /** * Attributes of the Blog Post block with this unique id. * * @param array $blocks Parsed blocks. * @param string $unique_id Block unique id. * @return array|null */ private static function find_block( $blocks, $unique_id ) { foreach ( $blocks as $block ) { if ( 'gutsliders/post-slider' === $block['blockName'] && ( $block['attrs']['uniqueId'] ?? '' ) === $unique_id ) { return $block['attrs']; } if ( ! empty( $block['innerBlocks'] ) ) { $found = self::find_block( $block['innerBlocks'], $unique_id ); if ( $found ) { return $found; } } } return null; } /* --------------------------------------------------------------------- * Cache * ------------------------------------------------------------------ */ /** * Current cache version. * * @return string */ private static function cache_version() { return (string) get_option( self::CACHE_VERSION_OPTION, '1' ); } /** * Invalidate every cached slider result (old entries simply expire). */ public static function flush_cache() { update_option( self::CACHE_VERSION_OPTION, (string) microtime( true ), true ); } /** * Publishing, unpublishing or scheduling changes what sliders show. * * @param string $new_status New status. * @param string $old_status Old status. */ public function on_status_change( $new_status, $old_status ) { if ( $new_status !== $old_status && ( 'publish' === $new_status || 'publish' === $old_status ) ) { self::flush_cache(); } } /** * A featured image change matters ("Only posts with a featured image"). * * @param mixed $meta_ids Meta ids. * @param int $object_id Post id. * @param string $meta_key Meta key. */ public function on_meta_change( $meta_ids, $object_id, $meta_key ) { if ( '_thumbnail_id' === $meta_key ) { self::flush_cache(); } } }