$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'], '…' ); } ?>
< class='bb-pfg-title'> >

have_posts() ) { $html = self::renderCards( $query, $a ); $count = (int) $query->post_count; } wp_reset_postdata(); $maxPages = (int) $query->max_num_pages; $hasMore = $a['showLoadMore'] && $paged < $maxPages; if ( '' === $html ) { $html = '

' . esc_html( $a['noPostsMessage'] ) . '

'; } wp_send_json_success( [ 'html' => $html, 'page' => $paged, 'hasMore' => $hasMore, 'count' => $count, ] ); } } new PostFilterGrid();