feat(widgets): category block AJAX + recommend/term render_html

Expose static HTML/query payloads for homepage category widgets,
tag cloud, categories, and term card/aggregation remote refresh.
This commit is contained in:
2026-07-20 22:57:10 +08:00
parent 2396ca98fb
commit 5a900d7385
6 changed files with 227 additions and 105 deletions
+35 -22
View File
@@ -22,14 +22,22 @@ class CategoriesWidget extends \WP_Widget {
);
}
public function widget($args, $instance): void {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
/** @return array<int,int> */
public static function parse_ids_static(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
}
public static function render_html(array $instance): string {
$count = max(1, min(50, (int) ($instance['count'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'count'));
if (!in_array($orderby, ['count', 'name', 'id', 'slug'], true)) {
@@ -40,8 +48,8 @@ class CategoriesWidget extends \WP_Widget {
$hierarchical = !empty($instance['hierarchical']);
$parent = max(0, (int) ($instance['parent'] ?? 0));
$show_count = !isset($instance['show_count']) || !empty($instance['show_count']);
$exclude = $this->parse_ids((string) ($instance['exclude'] ?? ''));
$include = $this->parse_ids((string) ($instance['include'] ?? ''));
$exclude = self::parse_ids_static((string) ($instance['exclude'] ?? ''));
$include = self::parse_ids_static((string) ($instance['include'] ?? ''));
$term_args = [
'taxonomy' => 'category',
@@ -65,11 +73,13 @@ class CategoriesWidget extends \WP_Widget {
$cats = get_categories($term_args);
if (!$cats) {
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
return function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无分类', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无分类', 'aksrt') . '</p>';
}
echo '<ul class="widget-list aksrt-cat-list">';
ob_start();
echo '<ul class="widget-list aksrt-cat-list ajax-item">';
foreach ($cats as $cat) {
echo '<li>';
echo '<a href="' . esc_url(get_category_link($cat->term_id)) . '">' . esc_html($cat->name) . '</a>';
@@ -79,23 +89,26 @@ class CategoriesWidget extends \WP_Widget {
echo '</li>';
}
echo '</ul>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$rest = rest_url('aksrt/v1/widgets/categories');
echo '<div class="ajaxpager" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($rest) . '">';
echo self::render_html($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/** @return array<int,int> */
private function parse_ids(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
return self::parse_ids_static($raw);
}
public function form($instance): void {
+81 -11
View File
@@ -323,33 +323,85 @@ class CategoryBlockWidget extends \WP_Widget {
);
}
public function widget($args, $instance): void {
$context = aksrt_home_widget_resolve_context($args);
$category_id = $this->resolve_category_id($instance);
$term = $category_id > 0 ? get_term($category_id, 'category') : null;
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
$title = $title !== '' ? $title : (($term && ! is_wp_error($term)) ? $term->name : __('精选分类', 'aksrt'));
/**
* @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int}
*/
public static function query_payload(array $instance, int $page = 1, string $context = 'homepage'): array {
$page = max(1, $page);
$category_id = self::resolve_category_id_static($instance);
$count = max(1, min(12, (int) ($instance['count'] ?? 6)));
$columns = aksrt_home_widget_effective_columns((int) ($instance['columns'] ?? 3), $context);
$layout = aksrt_home_widget_effective_layout(
aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'),
$context
);
$show_more = ! empty($instance['show_more']);
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'none'));
if (!in_array($paginate, ['ajax', 'none'], true)) {
$paginate = 'none';
}
$query_args = [
'post_type' => aksrt_home_widget_post_types(),
'post_status' => 'publish',
'posts_per_page' => $count,
'paged' => $page,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
if ($paginate === 'none') {
$query_args['no_found_rows'] = true;
}
if ($category_id > 0) {
$query_args['cat'] = $category_id;
}
$query = new \WP_Query($query_args);
ob_start();
aksrt_home_widget_render_post_grid($query, $layout, $columns, $context);
$html = (string) ob_get_clean();
if ($html === '' && $page === 1) {
$html = function_exists('aksrt_get_widget_null')
? aksrt_get_widget_null(__('暂无内容', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无内容', 'aksrt') . '</p>';
}
$max_pages = max(1, (int) $query->max_num_pages);
$total = (int) $query->found_posts;
$has_more = $paginate === 'ajax' && $page < $max_pages;
$paginate_html = '';
if ($paginate === 'ajax' && $has_more) {
$next = add_query_arg([
'page' => $page + 1,
'count' => $count,
'cat' => $category_id,
'columns' => $columns,
'layout' => $layout,
'context' => $context,
'paginate' => 'ajax',
], rest_url('aksrt/v1/widgets/category-block'));
$paginate_html = function_exists('aksrt_get_ajax_next_paginate')
? aksrt_get_ajax_next_paginate($total, $page, $count, $next)
: '<div class="text-center theme-pagination ajax-pag"><div class="next-page ajax-next"><a href="' . esc_url($next) . '">' . esc_html__('加载更多', 'aksrt') . '</a></div></div>';
} else {
$paginate_html = '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
}
return compact('html', 'paginate_html', 'has_more', 'page', 'max_pages', 'total');
}
public function widget($args, $instance): void {
$context = aksrt_home_widget_resolve_context($args);
$category_id = self::resolve_category_id_static($instance);
$term = $category_id > 0 ? get_term($category_id, 'category') : null;
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
$title = $title !== '' ? $title : (($term && ! is_wp_error($term)) ? $term->name : __('精选分类', 'aksrt'));
$show_more = ! empty($instance['show_more']);
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'none'));
if (!in_array($paginate, ['ajax', 'none'], true)) {
$paginate = 'none';
}
$instance['paginate'] = $paginate;
$payload = self::query_payload($instance, 1, $context);
echo $args['before_widget'];
echo '<section class="aksrt-home-widget aksrt-home-widget--category-block">';
@@ -360,7 +412,12 @@ class CategoryBlockWidget extends \WP_Widget {
__('查看分类', 'aksrt')
);
aksrt_home_widget_render_post_grid($query, $layout, $columns, $context);
$rest = rest_url('aksrt/v1/widgets/category-block');
echo '<div class="ajaxpager aksrt-home-widget__ajax" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($rest) . '">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
echo '</section>';
echo $args['after_widget'];
@@ -412,6 +469,14 @@ class CategoryBlockWidget extends \WP_Widget {
<input class="checkbox" type="checkbox" <?php checked($show_more); ?> id="<?php echo esc_attr($this->get_field_id('show_more')); ?>" name="<?php echo esc_attr($this->get_field_name('show_more')); ?>" value="1">
<label for="<?php echo esc_attr($this->get_field_id('show_more')); ?>"><?php esc_html_e('显示分类链接', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('分页', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="none" <?php selected(($instance['paginate'] ?? 'none'), 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
<option value="ajax" <?php selected(($instance['paginate'] ?? ''), 'ajax'); ?>><?php esc_html_e('AJAX', 'aksrt'); ?></option>
</select>
</label>
</p>
<?php if (function_exists('aksrt_widget_render_location_help')) { aksrt_widget_render_location_help('hybrid'); } ?>
<?php
}
@@ -424,10 +489,11 @@ class CategoryBlockWidget extends \WP_Widget {
'columns' => max(1, min(4, absint($new_instance['columns'] ?? 3))),
'layout' => aksrt_home_widget_resolve_layout($new_instance['layout'] ?? 'card', 'card'),
'show_more' => ! empty($new_instance['show_more']) ? 1 : 0,
'paginate' => sanitize_key((string) ($new_instance['paginate'] ?? 'none')) === 'ajax' ? 'ajax' : 'none',
];
}
private function resolve_category_id(array $instance): int {
public static function resolve_category_id_static(array $instance): int {
$category_id = absint($instance['category_id'] ?? 0);
if ($category_id > 0) {
return $category_id;
@@ -436,4 +502,8 @@ class CategoryBlockWidget extends \WP_Widget {
$fallback = aksrt_home_widget_fallback_category_ids(1);
return $fallback[0] ?? 0;
}
private function resolve_category_id(array $instance): int {
return self::resolve_category_id_static($instance);
}
}
+20 -18
View File
@@ -24,9 +24,7 @@ class CategoryRecommendWidget extends \WP_Widget {
);
}
public function widget($args, $instance): void {
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
$title = $title !== '' ? $title : __('分类推荐', 'aksrt');
public static function render_html(array $instance): string {
$category_ids = aksrt_home_widget_normalize_category_ids($instance['category_ids'] ?? [], 4);
$posts_per_cat = max(1, min(8, (int) ($instance['posts_per_category'] ?? 5)));
$columns = max(1, min(4, (int) ($instance['columns'] ?? 2)));
@@ -39,23 +37,16 @@ class CategoryRecommendWidget extends \WP_Widget {
$category_ids = aksrt_home_widget_fallback_category_ids(4);
}
$cards = $this->build_cards($category_ids, $posts_per_cat, $show_views, $show_description);
echo $args['before_widget'];
echo '<section class="aksrt-home-widget aksrt-home-widget--recommend">';
aksrt_home_widget_render_header($title);
$cards = self::build_cards_static($category_ids, $posts_per_cat, $show_views, $show_description);
if (empty($cards)) {
echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无推荐分类', 'aksrt')) : '<p class="widget-empty">' . esc_html__('暂无推荐分类', 'aksrt') . '</p>';
echo '</section>';
echo $args['after_widget'];
return;
return function_exists('aksrt_get_widget_null')
? aksrt_get_widget_null(__('暂无推荐分类', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无推荐分类', 'aksrt') . '</p>';
}
$lazy = aksrt_home_widget_get_lazyload_args();
printf('<div class="aksrt-category-recommend__grid" style="--aksrt-recommend-columns:%d;">', (int) $columns);
ob_start();
printf('<div class="aksrt-category-recommend__grid ajax-item" style="--aksrt-recommend-columns:%d;">', (int) $columns);
foreach ($cards as $card) {
?>
<article class="aksrt-category-recommend__card">
@@ -108,8 +99,19 @@ class CategoryRecommendWidget extends \WP_Widget {
</article>
<?php
}
echo '</div>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
$title = $title !== '' ? $title : __('分类推荐', 'aksrt');
$html = self::render_html($instance);
echo $args['before_widget'];
echo '<section class="aksrt-home-widget aksrt-home-widget--recommend">';
aksrt_home_widget_render_header($title);
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</section>';
echo $args['after_widget'];
}
@@ -190,7 +192,7 @@ class CategoryRecommendWidget extends \WP_Widget {
* @param bool $show_description Whether to fetch description.
* @return array<int, array<string, mixed>>
*/
private function build_cards(array $category_ids, int $posts_per_cat, bool $show_views, bool $show_description): array {
public static function build_cards_static(array $category_ids, int $posts_per_cat, bool $show_views, bool $show_description): array {
$cards = [];
foreach ($category_ids as $category_id) {
+19 -11
View File
@@ -22,13 +22,7 @@ class TagCloudWidget extends \WP_Widget {
);
}
public function widget($args, $instance): void {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public static function render_html(array $instance): string {
$taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'post_tag'));
if (!taxonomy_exists($taxonomy)) {
$taxonomy = 'post_tag';
@@ -46,18 +40,20 @@ class TagCloudWidget extends \WP_Widget {
$tags = get_terms([
'taxonomy' => $taxonomy,
'orderby' => $orderby,
'order' => 'DESC',
'order' => $orderby === 'name' ? 'ASC' : 'DESC',
'number' => $number,
'hide_empty' => !empty($instance['hide_empty']),
]);
if (is_wp_error($tags) || !$tags) {
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
return function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无标签', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无标签', 'aksrt') . '</p>';
}
$palette = ['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red', '', 'c-blue-2', 'c-yellow-2', 'c-green-2', 'c-purple-2', 'c-red-2', ''];
$i = random_int(0, 10);
$wrap = 'tag-cloud aksrt-tag-cloud author-tag' . ($fixed_width ? ' fixed-width' : '');
$wrap = 'tag-cloud aksrt-tag-cloud author-tag ajax-item' . ($fixed_width ? ' fixed-width' : '');
ob_start();
echo '<div class="' . esc_attr($wrap) . '">';
foreach ($tags as $tag) {
$url = get_term_link($tag);
@@ -79,7 +75,19 @@ class TagCloudWidget extends \WP_Widget {
echo '<a href="' . esc_url($url) . '" class="text-ellipsis ' . esc_attr(trim($cls)) . '"' . $tgt . '>' . esc_html($tag->name) . $count . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$rest = rest_url('aksrt/v1/widgets/tag-cloud');
echo '<div class="ajaxpager" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($rest) . '">';
echo self::render_html($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
+36 -22
View File
@@ -22,18 +22,32 @@ class WidgetTermAggregation extends \WP_Widget {
);
}
public function widget($args, $instance): void {
/** @return array<int,int> */
public static function parse_ids_static(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
}
public static function render_html(array $instance): string {
$taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'category'));
if (!taxonomy_exists($taxonomy)) {
$taxonomy = 'category';
}
$term_ids = $this->parse_ids((string) ($instance['term_ids'] ?? ''));
$term_ids = self::parse_ids_static((string) ($instance['term_ids'] ?? ''));
$count = max(1, min(12, (int) ($instance['count'] ?? 4)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'date'));
$columns = max(1, min(3, (int) ($instance['columns'] ?? 1)));
if (!$term_ids) {
// Fallback: top terms by count.
$terms = get_terms([
'taxonomy' => $taxonomy,
'number' => max(1, min(4, (int) ($instance['auto_terms'] ?? 2))),
@@ -42,18 +56,15 @@ class WidgetTermAggregation extends \WP_Widget {
'hide_empty' => true,
]);
if (is_wp_error($terms) || !$terms) {
return;
return function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无分类', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无分类', 'aksrt') . '</p>';
}
$term_ids = array_map(static fn($t) => (int) $t->term_id, $terms);
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<div class="aksrt-term-agg" style="--aksrt-ta-cols:' . esc_attr((string) $columns) . '">';
ob_start();
echo '<div class="aksrt-term-agg ajax-item" style="--aksrt-ta-cols:' . esc_attr((string) $columns) . '">';
foreach ($term_ids as $term_id) {
$term = get_term($term_id, $taxonomy);
if (!$term || is_wp_error($term)) {
@@ -118,23 +129,26 @@ class WidgetTermAggregation extends \WP_Widget {
echo '</div>';
}
echo '</div>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
$html = self::render_html($instance);
if ($html === '') {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/** @return array<int,int> */
private function parse_ids(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
return self::parse_ids_static($raw);
}
public function form($instance): void {
+36 -21
View File
@@ -22,14 +22,29 @@ class WidgetTermCards extends \WP_Widget {
);
}
public function widget($args, $instance): void {
/** @return array<int,int> */
public static function parse_ids_static(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
}
public static function render_html(array $instance): string {
$taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'category'));
if (!taxonomy_exists($taxonomy)) {
$taxonomy = 'category';
}
$number = max(1, min(30, (int) ($instance['number'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'count'));
$include = $this->parse_ids((string) ($instance['include'] ?? ''));
$include = self::parse_ids_static((string) ($instance['include'] ?? ''));
$hide_empty = !isset($instance['hide_empty']) || !empty($instance['hide_empty']);
$term_args = [
@@ -47,17 +62,14 @@ class WidgetTermCards extends \WP_Widget {
$terms = get_terms($term_args);
if (is_wp_error($terms) || !$terms) {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无分类', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无分类', 'aksrt') . '</p>';
}
$cols = max(1, min(4, (int) ($instance['columns'] ?? 2)));
echo '<div class="aksrt-term-cards" style="--aksrt-tc-cols:' . esc_attr((string) $cols) . '">';
ob_start();
echo '<div class="aksrt-term-cards ajax-item" style="--aksrt-tc-cols:' . esc_attr((string) $cols) . '">';
foreach ($terms as $term) {
$link = get_term_link($term);
if (is_wp_error($link)) {
@@ -88,23 +100,26 @@ class WidgetTermCards extends \WP_Widget {
echo '</div></a>';
}
echo '</div>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
$html = self::render_html($instance);
if ($html === '') {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/** @return array<int,int> */
private function parse_ids(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
return self::parse_ids_static($raw);
}
public function form($instance): void {