$max ) {
return (int) $max;
}
return $value;
}
/**
* Pick a value from an allowlist.
*
* @param mixed $value Raw value.
* @param string[] $allowed Allowed values.
* @param string $fallback Fallback.
* @return string
*/
public static function pickFrom( $value, array $allowed, $fallback ) {
$value = is_string( $value ) ? trim( $value ) : '';
return in_array( $value, $allowed, true ) ? $value : $fallback;
}
/**
* Normalize and sanitize the full attribute set into a safe, typed array.
*
* @param array $attributes Raw block attributes.
* @return array
*/
public static function resolveAttributes( array $attributes ) {
$postTypeRaw = isset( $attributes['postType'] ) ? sanitize_key( (string) $attributes['postType'] ) : 'post';
$postType = post_type_exists( $postTypeRaw ) ? $postTypeRaw : 'post';
$taxonomyRaw = isset( $attributes['filterTaxonomy'] ) ? sanitize_key( (string) $attributes['filterTaxonomy'] ) : 'category';
$taxonomy = taxonomy_exists( $taxonomyRaw ) ? $taxonomyRaw : 'category';
$columns = (array) ( $attributes['columns'] ?? [] );
$titleFont = (array) ( $attributes['titleFontSize'] ?? [] );
$readMoreLabel = isset( $attributes['readMoreLabel'] ) ? wp_strip_all_tags( (string) $attributes['readMoreLabel'] ) : '';
$readMoreLabel = '' !== trim( $readMoreLabel ) ? $readMoreLabel : __( 'Read More', 'b-blocks' );
$loadMoreLabel = isset( $attributes['loadMoreLabel'] ) ? wp_strip_all_tags( (string) $attributes['loadMoreLabel'] ) : '';
$loadMoreLabel = '' !== trim( $loadMoreLabel ) ? $loadMoreLabel : __( 'Load More', 'b-blocks' );
$allButtonLabel = isset( $attributes['allButtonLabel'] ) ? wp_strip_all_tags( (string) $attributes['allButtonLabel'] ) : '';
$allButtonLabel = '' !== trim( $allButtonLabel ) ? $allButtonLabel : __( 'All', 'b-blocks' );
$noPostsMessage = isset( $attributes['noPostsMessage'] ) ? wp_strip_all_tags( (string) $attributes['noPostsMessage'] ) : '';
$noPostsMessage = '' !== trim( $noPostsMessage ) ? $noPostsMessage : __( 'No posts found.', 'b-blocks' );
return [
'postType' => $postType,
'postsPerPage' => self::clampInt( $attributes['postsPerPage'] ?? 6, 1, 24, 6 ),
'defaultCategory' => absint( $attributes['defaultCategory'] ?? 0 ),
'orderBy' => self::pickFrom( $attributes['orderBy'] ?? 'date', self::ORDERBY, 'date' ),
'order' => self::pickFrom( strtoupper( (string) ( $attributes['order'] ?? 'desc' ) ), self::ORDER, 'DESC' ),
'excludeCurrentPost' => ! empty( $attributes['excludeCurrentPost'] ),
'currentPostId' => isset( $attributes['currentPostId'] ) ? absint( $attributes['currentPostId'] ) : 0,
'showFilterBar' => ! isset( $attributes['showFilterBar'] ) || (bool) $attributes['showFilterBar'],
'filterTaxonomy' => $taxonomy,
'showAllButton' => ! isset( $attributes['showAllButton'] ) || (bool) $attributes['showAllButton'],
'allButtonLabel' => $allButtonLabel,
'layout' => self::pickFrom( $attributes['layout'] ?? 'grid', [ 'grid', 'masonry' ], 'grid' ),
'columnsDesktop' => self::clampInt( $columns['desktop'] ?? 3, 1, 6, 3 ),
'columnsTablet' => self::clampInt( $columns['tablet'] ?? 2, 1, 4, 2 ),
'columnsMobile' => self::clampInt( $columns['mobile'] ?? 1, 1, 2, 1 ),
'columnGap' => self::clampInt( $attributes['columnGap'] ?? 24, 0, 80, 24 ),
'rowGap' => self::clampInt( $attributes['rowGap'] ?? 24, 0, 80, 24 ),
'cardStyle' => self::pickFrom( $attributes['cardStyle'] ?? 'boxed', [ 'boxed', 'flat' ], 'boxed' ),
'cardRadius' => self::clampInt( $attributes['cardRadius'] ?? 8, 0, 32, 8 ),
'cardBackground' => self::sanitizeColor( $attributes['cardBackground'] ?? '', '#ffffff' ),
'cardBorderColor' => self::sanitizeColor( $attributes['cardBorderColor'] ?? '', '#e5e7eb' ),
'cardShadow' => self::pickFrom( $attributes['cardShadow'] ?? 'sm', [ 'none', 'sm', 'md', 'lg' ], 'sm' ),
'showFeaturedImage' => ! isset( $attributes['showFeaturedImage'] ) || (bool) $attributes['showFeaturedImage'],
'imageRatio' => self::pickFrom( $attributes['imageRatio'] ?? '16/9', self::RATIOS, '16/9' ),
'imageRadius' => self::clampInt( $attributes['imageRadius'] ?? 8, 0, 32, 8 ),
'showCategory' => ! isset( $attributes['showCategory'] ) || (bool) $attributes['showCategory'],
'showTitle' => ! isset( $attributes['showTitle'] ) || (bool) $attributes['showTitle'],
'titleTag' => self::pickFrom( $attributes['titleTag'] ?? 'h3', self::TITLE_TAGS, 'h3' ),
'titleLines' => self::clampInt( $attributes['titleLines'] ?? 0, 0, 4, 0 ),
'showExcerpt' => ! isset( $attributes['showExcerpt'] ) || (bool) $attributes['showExcerpt'],
'excerptLength' => self::clampInt( $attributes['excerptLength'] ?? 20, 5, 50, 20 ),
'showMeta' => ! isset( $attributes['showMeta'] ) || (bool) $attributes['showMeta'],
'showAuthor' => ! isset( $attributes['showAuthor'] ) || (bool) $attributes['showAuthor'],
'showDate' => ! isset( $attributes['showDate'] ) || (bool) $attributes['showDate'],
'showReadMore' => ! isset( $attributes['showReadMore'] ) || (bool) $attributes['showReadMore'],
'readMoreLabel' => $readMoreLabel,
'showLoadMore' => ! empty( $attributes['showLoadMore'] ),
'loadMoreLabel' => $loadMoreLabel,
'noPostsMessage' => $noPostsMessage,
'accentColor' => self::sanitizeColor( $attributes['accentColor'] ?? '', '#146EF5' ),
'accentTextColor' => self::sanitizeColor( $attributes['accentTextColor'] ?? '', '#ffffff' ),
'titleColor' => self::sanitizeColor( $attributes['titleColor'] ?? '', 'inherit' ),
'excerptColor' => self::sanitizeColor( $attributes['excerptColor'] ?? '', 'inherit' ),
'metaColor' => self::sanitizeColor( $attributes['metaColor'] ?? '', 'inherit' ),
'titleSizeDesktop' => self::clampInt( preg_replace( '/[^0-9]/', '', (string) ( $titleFont['desktop'] ?? '18' ) ), 12, 48, 18 ),
'titleSizeTablet' => self::clampInt( preg_replace( '/[^0-9]/', '', (string) ( $titleFont['tablet'] ?? '17' ) ), 12, 40, 17 ),
'titleSizeMobile' => self::clampInt( preg_replace( '/[^0-9]/', '', (string) ( $titleFont['mobile'] ?? '16' ) ), 12, 36, 16 ),
];
}
/* ----------------------------------------------------------------------
* Query
* ------------------------------------------------------------------- */
/**
* Build sanitized WP_Query args.
*
* @param array $a Resolved attributes.
* @param int $termId Term ID to filter by (0 = none).
* @param int $paged Page number (1-based).
* @return array
*/
public static function buildQueryArgs( array $a, $termId, $paged ) {
$termId = absint( $termId );
$paged = max( 1, absint( $paged ) );
$args = [
'post_type' => $a['postType'],
'posts_per_page' => $a['postsPerPage'],
'paged' => $paged,
'post_status' => 'publish',
'orderby' => $a['orderBy'],
'order' => $a['order'],
'has_password' => false,
'ignore_sticky_posts' => true,
];
if ( $a['excludeCurrentPost'] && $a['currentPostId'] > 0 ) {
$args['post__not_in'] = [ $a['currentPostId'] ];
}
if ( $termId > 0 && taxonomy_exists( $a['filterTaxonomy'] ) ) {
$args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
[
'taxonomy' => $a['filterTaxonomy'],
'field' => 'term_id',
'terms' => [ $termId ],
],
];
}
return $args;
}
/* ----------------------------------------------------------------------
* Card rendering
* ------------------------------------------------------------------- */
/**
* Render the cards for a query as an escaped HTML fragment.
*
* @param \WP_Query $query The query.
* @param array $a Resolved attributes.
* @return string Escaped HTML.
*/
public static function renderCards( $query, array $a ) {
ob_start();
while ( $query->have_posts() ) :
$query->the_post();
$postId = get_the_ID();
$permalink = get_permalink( $postId );
$titleText = get_the_title( $postId );
// Featured image.
$imageUrl = '';
$imageAlt = '';
if ( $a['showFeaturedImage'] && has_post_thumbnail( $postId ) ) {
$thumbUrl = get_the_post_thumbnail_url( $postId, 'large' );
if ( $thumbUrl ) {
$imageUrl = $thumbUrl;
$thumbId = get_post_thumbnail_id( $postId );
if ( $thumbId ) {
$metaAlt = get_post_meta( $thumbId, '_wp_attachment_image_alt', true );
if ( is_string( $metaAlt ) ) {
$imageAlt = trim( wp_strip_all_tags( $metaAlt ) );
}
}
if ( '' === $imageAlt ) {
$imageAlt = $titleText;
}
}
}
// Primary term from the filter taxonomy.
$termName = '';
$termLink = '';
if ( $a['showCategory'] ) {
$terms = get_the_terms( $postId, $a['filterTaxonomy'] );
if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
$firstTerm = $terms[0];
$termName = $firstTerm->name;
$link = get_term_link( $firstTerm );
if ( ! is_wp_error( $link ) ) {
$termLink = $link;
}
}
}
// Excerpt.
$excerptText = '';
if ( $a['showExcerpt'] ) {
$excerptText = wp_trim_words( get_the_excerpt( $postId ), $a['excerptLength'], '…' );
}
?>
' . esc_html( $a['noPostsMessage'] ) . '
'; } wp_send_json_success( [ 'html' => $html, 'page' => $paged, 'hasMore' => $hasMore, 'count' => $count, ] ); } } new PostFilterGrid();