fix(widgets): restore article list rendering and reference styles
This commit is contained in:
1 parent
4ea00f1322
commit
fc04bf65e6
18 files changed
+345
-97
No files matched your search
+29
-28
@@ -68,9 +68,11 @@
|
||||
.dark-theme .zib-widget-wrap, [data-theme="dark"] .zib-widget-wrap { background-color:var(--aksrt-widget-bg-dark,var(--aksrt-widget-bg-light,transparent)); background-image:var(--aksrt-widget-bg-image-dark,var(--aksrt-widget-bg-image-light,none)); }
|
||||
.zib-widget-wrap .widget-container { position:relative; }
|
||||
.aksrt-user-widget-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible,
|
||||
.aksrt-tab-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline:3px solid var(--focus-color,#ff5473) !important; outline-offset:3px; }
|
||||
.aksrt-tab-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible,
|
||||
.aksrt-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline:3px solid var(--focus-color,#ff5473) !important; outline-offset:3px; }
|
||||
.dark-theme :is(.aksrt-user-widget-wrap, .aksrt-tab-posts-wrap) :where(a, button, input, select, textarea, [tabindex]):focus-visible,
|
||||
[data-theme="dark"] :is(.aksrt-user-widget-wrap, .aksrt-tab-posts-wrap) :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline-color:#ffd166 !important; }
|
||||
.dark-theme .aksrt-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible,
|
||||
[data-theme="dark"] :is(.aksrt-user-widget-wrap, .aksrt-tab-posts-wrap, .aksrt-posts-wrap) :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline-color:#ffd166 !important; }
|
||||
|
||||
.user-card.zib-widget { padding:0; overflow:hidden; }
|
||||
.user-card.widget .user-info > .text-center > p:not(.muted-color) { white-space:nowrap; }
|
||||
@@ -159,21 +161,21 @@
|
||||
.widget-tab-post, .term-aggregation { line-height:1.42857143; }
|
||||
/* Public typography overrides Bootstrap headings. Match the reference widget
|
||||
* without changing article headings elsewhere on the page. */
|
||||
.aksrt-tab-posts-wrap { font-size:14px; line-height:1.42857143; color:var(--main-color,#4e5358); }
|
||||
.widget-tab-post .item-heading { font-weight:500; color:inherit; }
|
||||
.widget-tab-post .item-heading.font-bold { font-weight:700; }
|
||||
.widget-tab-post .posts-mini { vertical-align:baseline; }
|
||||
.widget-tab-post .posts-mini-con { flex:auto; }
|
||||
.widget-tab-post .item-meta .meta-pay { padding:0 4px; cursor:auto; }
|
||||
.aksrt-tab-posts-wrap, .aksrt-posts-wrap { font-size:14px; line-height:1.42857143; color:var(--main-color,#4e5358); }
|
||||
.widget-tab-post .item-heading, .widget-main-post .item-heading { font-weight:500; color:inherit; }
|
||||
.widget-tab-post .item-heading.font-bold, .widget-main-post .item-heading.font-bold { font-weight:700; }
|
||||
.widget-tab-post .posts-mini, .widget-main-post .posts-mini { vertical-align:baseline; }
|
||||
.widget-tab-post .posts-mini-con, .widget-main-post .posts-mini-con { flex:auto; }
|
||||
.widget-tab-post .item-meta .meta-pay, .widget-main-post .item-meta .meta-pay { padding:0 4px; cursor:auto; }
|
||||
.widget-tab-post .post_ajax_loader { min-height: 40px; }
|
||||
/* Keep the reference's compact pagination after public pagination.css. */
|
||||
.widget-tab-post .pagenav {
|
||||
.widget-tab-post .pagenav, .widget-main-post .pagenav {
|
||||
display:block;
|
||||
padding:20px 0;
|
||||
line-height:1.4;
|
||||
text-align:center;
|
||||
}
|
||||
.widget-tab-post .pagenav .page-numbers {
|
||||
.widget-tab-post .pagenav .page-numbers, .widget-main-post .pagenav .page-numbers {
|
||||
display:inline-block;
|
||||
min-width:31px;
|
||||
min-height:0;
|
||||
@@ -187,8 +189,8 @@
|
||||
line-height:inherit;
|
||||
font-weight:400;
|
||||
}
|
||||
.widget-tab-post .pagenav a.page-numbers:hover { background:var(--main-border-color); color:var(--muted-color); opacity:.7; }
|
||||
.widget-tab-post .pagenav .page-numbers > span {
|
||||
.widget-tab-post .pagenav a.page-numbers:hover, .widget-main-post .pagenav a.page-numbers:hover { background:var(--main-border-color); color:var(--muted-color); opacity:.7; }
|
||||
.widget-tab-post .pagenav .page-numbers > span, .widget-main-post .pagenav .page-numbers > span {
|
||||
display:inline;
|
||||
min-width:0;
|
||||
min-height:0;
|
||||
@@ -201,12 +203,12 @@
|
||||
font-size:inherit;
|
||||
line-height:inherit;
|
||||
}
|
||||
.widget-tab-post .pagenav .page-numbers > .ml6 { margin-left:6px; }
|
||||
.widget-tab-post .pagenav .page-numbers > .mr6 { margin-right:6px; }
|
||||
.widget-tab-post .pagenav .prev i,
|
||||
.widget-tab-post .pagenav .next i { font-size:1.2em; }
|
||||
.widget-tab-post .pagenav .pag-jump { display:inline-flex; position:relative; width:auto; align-items:center; justify-content:normal; gap:0; }
|
||||
.widget-tab-post .pagenav .pag-jump .jump-input {
|
||||
.widget-tab-post .pagenav .page-numbers > .ml6, .widget-main-post .pagenav .page-numbers > .ml6 { margin-left:6px; }
|
||||
.widget-tab-post .pagenav .page-numbers > .mr6, .widget-main-post .pagenav .page-numbers > .mr6 { margin-right:6px; }
|
||||
.widget-tab-post .pagenav .prev i, .widget-main-post .pagenav .prev i,
|
||||
.widget-tab-post .pagenav .next i, .widget-main-post .pagenav .next i { font-size:1.2em; }
|
||||
.widget-tab-post .pagenav .pag-jump, .widget-main-post .pagenav .pag-jump { display:inline-flex; position:relative; width:auto; align-items:center; justify-content:normal; gap:0; }
|
||||
.widget-tab-post .pagenav .pag-jump .jump-input, .widget-main-post .pagenav .pag-jump .jump-input {
|
||||
position:absolute;
|
||||
inset:0;
|
||||
width:100%;
|
||||
@@ -217,23 +219,22 @@
|
||||
opacity:0;
|
||||
transition:.2s;
|
||||
}
|
||||
.widget-tab-post .pagenav .jump-input:focus,
|
||||
.widget-tab-post .pagenav .pag-jump:hover .jump-input { opacity:1; }
|
||||
.sidebar .widget-tab-post .pagenav { padding:10px 5px; }
|
||||
.sidebar .widget-tab-post .theme-pagination { margin:10px; }
|
||||
.sidebar .widget-tab-post .pagenav .page-numbers { min-width:28px; margin:2px; font-size:12px; }
|
||||
.widget-tab-post .pagenav .jump-input:focus, .widget-main-post .pagenav .jump-input:focus,
|
||||
.widget-tab-post .pagenav .pag-jump:hover .jump-input, .widget-main-post .pagenav .pag-jump:hover .jump-input { opacity:1; }
|
||||
.sidebar :is(.widget-tab-post, .widget-main-post) .pagenav { padding:10px 5px; }
|
||||
.sidebar :is(.widget-tab-post, .widget-main-post) .theme-pagination { margin:10px; }
|
||||
.sidebar :is(.widget-tab-post, .widget-main-post) .pagenav .page-numbers { min-width:28px; margin:2px; font-size:12px; }
|
||||
@media (max-width:640px) {
|
||||
.widget-tab-post .pagenav a.page-numbers { display:none; }
|
||||
.widget-tab-post .pagenav a.next,
|
||||
.widget-tab-post .pagenav a.prev { display:inline-block; }
|
||||
.widget-tab-post .pagenav .page-numbers { min-width:28px; margin:2px; font-size:12px; }
|
||||
:is(.widget-tab-post, .widget-main-post) .pagenav a.page-numbers { display:none; }
|
||||
:is(.widget-tab-post, .widget-main-post) .pagenav a.next,
|
||||
:is(.widget-tab-post, .widget-main-post) .pagenav a.prev { display:inline-block; }
|
||||
:is(.widget-tab-post, .widget-main-post) .pagenav .page-numbers { min-width:28px; margin:2px; font-size:12px; }
|
||||
}
|
||||
/* Widget pagination mirrors the reference .ajax-next rules while staying on the REST client. */
|
||||
.theme-pagination { margin: 20px 0; }
|
||||
.theme-pagination .next-page a { border-radius: 30px; padding: 5px 15px; color: var(--muted-color, #8a8c98); opacity: .6; cursor: pointer; }
|
||||
.theme-pagination .next-page a:hover { background: var(--main-border-color, rgba(127,127,127,.18)); opacity: 1; }
|
||||
.pagenav .page-numbers { cursor: pointer; }
|
||||
.widget-ajaxpager[lazyload-action="ias"] .post_ajax_trigger { display: none; }
|
||||
/* 组合模块 PC 端为左右两列;后台预览不加载 main.css 的 .flex 工具类,这里需自声明。 */
|
||||
.widget-slider-graphic { display: flex; gap: 10px; align-items: stretch; }
|
||||
.widget-slider-graphic .slider-col { width: var(--layout-ratio, 60%); min-width: 0; }
|
||||
|
||||
@@ -85,10 +85,17 @@
|
||||
}
|
||||
|
||||
function itemsUrl(widget, instance, page, segment) {
|
||||
var base = String(config.restUrl || '').replace(/\/$/, '');
|
||||
var url = base + '/' + encodeURIComponent(widget) + '/items?instance=' + encodeURIComponent(instance) + '&page=' + encodeURIComponent(page);
|
||||
if (segment !== undefined && segment !== '') url += '&segment=' + encodeURIComponent(segment);
|
||||
return url;
|
||||
var url = new URL(config.restUrl, window.location.href);
|
||||
var path = '/' + encodeURIComponent(widget) + '/items';
|
||||
if (url.searchParams.has('rest_route')) {
|
||||
url.searchParams.set('rest_route', url.searchParams.get('rest_route').replace(/\/$/, '') + path);
|
||||
} else {
|
||||
url.pathname = url.pathname.replace(/\/$/, '') + path;
|
||||
}
|
||||
url.searchParams.set('instance', instance);
|
||||
url.searchParams.set('page', page);
|
||||
if (segment !== undefined && segment !== '') url.searchParams.set('segment', segment);
|
||||
return url.href;
|
||||
}
|
||||
|
||||
function loaderOf(container) {
|
||||
@@ -192,6 +199,14 @@
|
||||
|
||||
function initItems(container) {
|
||||
if (!once(container, 'Items')) return;
|
||||
if (container.hasAttribute('data-widget-lazy') && container.dataset.loaded !== '1') {
|
||||
container.addEventListener('lazybeforeunveil', function (event) {
|
||||
if (event.target === container && container.dataset.loaded !== '1') loadItems(container, 1, false);
|
||||
});
|
||||
// main.js loads asynchronously through loader_js. Its lazySizes scan
|
||||
// may finish before this client binds, so consume an unveiled pager.
|
||||
if (!container.classList.contains('lazyload')) loadItems(container, 1, false);
|
||||
}
|
||||
function jumpTo(input) {
|
||||
var page = Math.min(Number(input.max), Math.floor(Number(input.value)));
|
||||
if (!Number.isFinite(page) || page < 1 || page === Number(container.dataset.page || 1)) {
|
||||
@@ -229,11 +244,8 @@
|
||||
return;
|
||||
}
|
||||
if (trigger) {
|
||||
// data-widget-ias marks the hidden reveal trigger the runtime clicks
|
||||
// when the lazy container scrolls into view; it reloads page one.
|
||||
event.preventDefault();
|
||||
if (trigger.dataset.widgetIas === '1') loadItems(container, 1, false);
|
||||
else loadItems(container, Number(trigger.dataset.nextPage || 2), true);
|
||||
loadItems(container, Number(trigger.dataset.nextPage || 2), true);
|
||||
return;
|
||||
}
|
||||
if (pageLink) {
|
||||
@@ -346,6 +358,7 @@
|
||||
var scope = root || document;
|
||||
scope.querySelectorAll('.aksrt-widget-video[data-video-url]').forEach(initVideo);
|
||||
scope.querySelectorAll('.aksrt-widget-quote').forEach(initQuote);
|
||||
if (scope.matches && scope.matches('[data-widget-items]')) initItems(scope);
|
||||
scope.querySelectorAll('[data-widget-items]').forEach(initItems);
|
||||
scope.querySelectorAll('.widget-tab-post').forEach(initTabs);
|
||||
scope.querySelectorAll('.aksrt-faq').forEach(initFaq);
|
||||
|
||||
@@ -23,19 +23,18 @@ final class FrontendRenderer {
|
||||
}
|
||||
|
||||
AssetManager::enqueue_for($definition);
|
||||
if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts'], true)) {
|
||||
if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts', 'posts'], true)) {
|
||||
self::render_reference_widget($definition, $instance, $widget, $content);
|
||||
return;
|
||||
}
|
||||
if (in_array($definition->slug(), [
|
||||
'post_pay', 'posts', 'oneline_posts', 'hot_posts',
|
||||
'post_pay', 'oneline_posts', 'hot_posts',
|
||||
'slider', 'slider_media_combo', 'icon_cards', 'icon_cover_cards',
|
||||
'buttons', 'notice', 'term_cards', 'term_aggregation', 'media_cards',
|
||||
'combo_info', 'partners', 'recent_comments', 'posts_toc',
|
||||
], true)) {
|
||||
echo (string) ($args['before_widget'] ?? '');
|
||||
if ($definition->slug() !== 'post_pay') echo self::reference_title($instance);
|
||||
$animation = '';
|
||||
$content_classes = ['widget-content'];
|
||||
$device_class = WidgetVisibility::device_class($instance);
|
||||
if ($device_class !== '') {
|
||||
@@ -107,7 +106,8 @@ final class FrontendRenderer {
|
||||
}
|
||||
|
||||
$style = self::style($instance);
|
||||
echo '<div class="' . esc_attr(implode(' ', $classes)) . '" data-aksrt-widget="' . esc_attr($definition->slug()) . '" data-widget-id="' . esc_attr((string) $widget->id) . '"' . ($style !== '' ? ' style="' . esc_attr($style) . '"' : '') . '>';
|
||||
$affix_attr = !empty($instance['sidebar_affix']) ? ' data-affix="true"' : '';
|
||||
echo '<div class="' . esc_attr(implode(' ', $classes)) . '" data-aksrt-widget="' . esc_attr($definition->slug()) . '" data-widget-id="' . esc_attr((string) $widget->id) . '"' . $affix_attr . ($style !== '' ? ' style="' . esc_attr($style) . '"' : '') . '>';
|
||||
echo '<div class="widget-container"><div>';
|
||||
echo self::reference_title($instance); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '<div class="' . esc_attr(implode(' ', $content_classes)) . '">' . $content . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
@@ -24,7 +24,7 @@ final class PostQuery {
|
||||
'ignore_sticky_posts' => true,
|
||||
'no_found_rows' => false,
|
||||
];
|
||||
if (array_key_exists('paginate', $config) && in_array((string) $config['paginate'], ['', 'none'], true)) {
|
||||
if (isset($config['paginate']) && $config['paginate'] === '') {
|
||||
$args['no_found_rows'] = true;
|
||||
$args['paged'] = 1;
|
||||
}
|
||||
@@ -33,13 +33,22 @@ final class PostQuery {
|
||||
// category_ids remains available only to the unrelated generic posts
|
||||
// widgets; a tab segment must never fall back to it.
|
||||
$is_reference_segment = array_key_exists('cat', $segment);
|
||||
$cat_raw = array_key_exists('cat', $config) ? (string) $config['cat'] : ($is_reference_segment ? '' : (string) ($config['category_ids'] ?? ''));
|
||||
$categories = self::signed_ids($cat_raw);
|
||||
if (!empty($config['category_id'])) $categories = [(int) $config['category_id']];
|
||||
$cat_in = array_values(array_filter($categories, static fn(int $id): bool => $id > 0));
|
||||
$cat_not = array_values(array_map('abs', array_filter($categories, static fn(int $id): bool => $id < 0)));
|
||||
if ($cat_in) $args['category__in'] = $cat_in;
|
||||
if ($cat_not) $args['category__not_in'] = $cat_not;
|
||||
if (array_key_exists('cat', $config)) {
|
||||
// WP_Query's `cat` expands included parents to descendants and
|
||||
// applies negative IDs with the same reference-theme semantics.
|
||||
$categories = self::signed_ids((string) $config['cat']);
|
||||
if ($categories) {
|
||||
$args['cat'] = implode(',', $categories);
|
||||
}
|
||||
} else {
|
||||
// Generic widgets retain their explicit category ID/list behavior.
|
||||
$categories = self::signed_ids((string) ($config['category_ids'] ?? ''));
|
||||
if (!empty($config['category_id'])) $categories = [(int) $config['category_id']];
|
||||
$cat_in = array_values(array_filter($categories, static fn(int $id): bool => $id > 0));
|
||||
$cat_not = array_values(array_map('abs', array_filter($categories, static fn(int $id): bool => $id < 0)));
|
||||
if ($cat_in) $args['category__in'] = $cat_in;
|
||||
if ($cat_not) $args['category__not_in'] = $cat_not;
|
||||
}
|
||||
$tags = $is_reference_segment ? [] : RendererHelpers::ids((string) ($config['tag_ids'] ?? ''));
|
||||
if ($tags) {
|
||||
$args['tag__in'] = $tags;
|
||||
@@ -47,14 +56,15 @@ final class PostQuery {
|
||||
|
||||
$days = max(0, (int) ($config['limit_day'] ?? 0));
|
||||
if ($days > 0) {
|
||||
$now = function_exists('current_time') ? current_time('timestamp') : time();
|
||||
$format = static function (int $timestamp): string {
|
||||
return function_exists('wp_date') ? wp_date('Y-m-d H:i:s', $timestamp) : date('Y-m-d H:i:s', $timestamp);
|
||||
};
|
||||
$args['date_query'] = [['after' => $format($now - ($days * DAY_IN_SECONDS)), 'before' => $format($now), 'inclusive' => true]];
|
||||
$now = current_datetime();
|
||||
$args['date_query'] = [[
|
||||
'after' => $now->modify('-' . $days . ' days')->format('Y-m-d H:i:s'),
|
||||
'before' => $now->format('Y-m-d H:i:s'),
|
||||
'inclusive' => true,
|
||||
]];
|
||||
}
|
||||
self::apply_order($args, (string) ($config['orderby'] ?? 'date'), (string) ($config['order'] ?? 'DESC'));
|
||||
$pay = $is_reference_segment ? ($config['zibpay_type'] ?? []) : ($config['pay_type'] ?? '');
|
||||
$pay = array_key_exists('zibpay_type', $config) ? $config['zibpay_type'] : ($config['pay_type'] ?? '');
|
||||
self::apply_pay_type($args, $pay);
|
||||
return $args;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ final class PostRenderer {
|
||||
}
|
||||
|
||||
public static function payload(array $instance, string $slug, int $page = 1, string $segment = ''): array {
|
||||
$page = max(1, $page);
|
||||
if (($instance['paginate'] ?? '') === '') {
|
||||
$page = 1;
|
||||
}
|
||||
$segment_config = $slug === 'tab_posts' ? PostQuery::segment($instance, $segment) : [];
|
||||
$query = PostQuery::build($instance, $page, $segment_config);
|
||||
$html = self::items($query, $instance, $slug, $page);
|
||||
@@ -47,9 +51,7 @@ final class PostRenderer {
|
||||
'is_tag' => false,
|
||||
'is_author' => false,
|
||||
'is_search' => false,
|
||||
'is_mult_thumb' => 'disable',
|
||||
'is_no_thumb' => 'disable',
|
||||
'show_number' => in_array('show_number', $mini_opt, true),
|
||||
'show_number' => $page === 1 && in_array('show_number', $mini_opt, true),
|
||||
'show_thumb' => in_array('show_thumb', $mini_opt, true),
|
||||
'show_meta' => in_array('show_meta', $mini_opt, true),
|
||||
'number_offset' => ($page - 1) * max(1, (int) ($instance['count'] ?? 8)),
|
||||
@@ -60,6 +62,11 @@ final class PostRenderer {
|
||||
if ($mode === 'mini') {
|
||||
return aksrt_render_mini_list($query, array_merge($config, ['class' => 'ajax-item']), false);
|
||||
}
|
||||
if ($mode === 'list') {
|
||||
// Zibll suppresses archive-only thumb variants in widget list mode.
|
||||
$config['is_mult_thumb'] = 'disable';
|
||||
$config['is_no_thumb'] = 'disable';
|
||||
}
|
||||
// The .widget-ajaxpager container rendered by the caller is the ajax
|
||||
// target, so item markup goes in without the page-level list wrapper.
|
||||
return aksrt_render_post_list(array_merge($config, ['skip_wrapper' => true]), $query, false);
|
||||
@@ -78,11 +85,12 @@ final class PostRenderer {
|
||||
'show_number' => false,
|
||||
])) : '';
|
||||
} else {
|
||||
$items .= aksrt_render_post(array_merge($config, [
|
||||
'type' => $style === 'card' ? 'card' : 'list',
|
||||
'is_mult_thumb' => 'disable',
|
||||
'is_no_thumb' => 'disable',
|
||||
]));
|
||||
$item_config = array_merge($config, ['type' => $style === 'card' ? 'card' : 'list']);
|
||||
if ($style === 'list') {
|
||||
$item_config['is_mult_thumb'] = 'disable';
|
||||
$item_config['is_no_thumb'] = 'disable';
|
||||
}
|
||||
$items .= aksrt_render_post($item_config);
|
||||
}
|
||||
}
|
||||
wp_reset_postdata();
|
||||
@@ -98,13 +106,13 @@ final class PostRenderer {
|
||||
*/
|
||||
private static function pagination(array $instance, int $page, int $max_pages): string {
|
||||
$mode = (string) ($instance['paginate'] ?? '');
|
||||
if ($mode === '' || $mode === 'none') {
|
||||
if ($mode === '') {
|
||||
return '<div class="ajax-pag hide"><div class="next-page"><a href="#"></a></div></div>';
|
||||
}
|
||||
if ($max_pages <= 1) {
|
||||
return '';
|
||||
}
|
||||
if ($mode === 'number' || $mode === 'numbered') {
|
||||
if ($mode === 'number') {
|
||||
$links = '';
|
||||
$mobile = wp_is_mobile();
|
||||
$mid_size = $mobile ? 0 : 2;
|
||||
|
||||
@@ -47,13 +47,11 @@ final class ContentRenderer {
|
||||
if ($segment !== '') {
|
||||
$attrs .= ' data-segment="' . esc_attr($segment) . '"';
|
||||
}
|
||||
$loader = '<div class="post_ajax_loader" role="status" aria-label="' . esc_attr__('正在加载', 'aksrt') . '">' . self::placeholder($instance) . '</div>';
|
||||
if (($instance['load_mode'] ?? '') === 'ajax') {
|
||||
// The runtime (lazysizes + lazyload-action) reveals this container
|
||||
// and clicks the single .ias-btn trigger; frontend.js then loads
|
||||
// the first page through the REST client.
|
||||
$trigger = '<span class="hide aksrt-ias-trigger"><a class="ias-btn" no-scroll="true" href="javascript:void(0)" data-widget-ias="1" data-next-page="1" aria-hidden="true" tabindex="-1"></a></span>';
|
||||
return '<div class="widget-ajaxpager lazyload" lazyload-action="ias"' . $attrs . ' aria-live="polite" aria-busy="true">' . $trigger . $loader . '</div>';
|
||||
$lazy = ($instance['load_mode'] ?? '') === 'ajax';
|
||||
$loader = '<div class="post_ajax_loader' . ($lazy ? ' is-loading' : '') . '" role="status" aria-label="' . esc_attr__('正在加载', 'aksrt') . '">' . self::placeholder($instance) . '</div>';
|
||||
if ($lazy) {
|
||||
// lazySizes reveals the pager; the widget client owns its request.
|
||||
return '<div class="widget-ajaxpager lazyload" data-widget-lazy' . $attrs . ' aria-live="polite" aria-busy="true">' . $loader . '</div>';
|
||||
}
|
||||
$payload = PostRenderer::payload($instance, $slug, $page, $segment);
|
||||
// The loader must live inside the pager: the REST client in frontend.js
|
||||
|
||||
@@ -34,7 +34,7 @@ final class RestController extends \WP_REST_Controller {
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function get_items($request) {
|
||||
$unknown = array_diff(array_keys($request->get_params()), ['widget', 'instance', 'page', 'segment']);
|
||||
$unknown = array_diff(array_keys($request->get_params()), ['widget', 'instance', 'page', 'segment', 'rest_route']);
|
||||
if ($unknown) {
|
||||
return new \WP_Error('aksrt_widget_unknown_parameter', __('请求包含不支持的参数。', 'aksrt'), ['status' => 400, 'parameters' => array_values($unknown)]);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ final class WidgetCatalog {
|
||||
'size' => $size,
|
||||
'common' => [
|
||||
'title' => !in_array($slug, ['post_pay', 'layout_gap', 'slider', 'slider_media_combo', 'buttons', 'posts_toc'], true),
|
||||
'affix' => ($size === 'sidebar' && $slug !== 'posts_toc') || in_array($slug, ['buttons', 'notice'], true),
|
||||
'affix' => ($size === 'sidebar' && $slug !== 'posts_toc') || in_array($slug, ['buttons', 'notice', 'posts'], true),
|
||||
'animation' => !in_array($slug, ['buttons', 'term_cards', 'term_aggregation', 'media_cards', 'combo_info', 'slider_media_combo'], true),
|
||||
],
|
||||
'fields' => WidgetSchemas::for($slug),
|
||||
|
||||
@@ -32,8 +32,8 @@ test('term aggregation renders the real SVG use element, total count, and config
|
||||
const firstQuery = fixture.aggregation_queries[0];
|
||||
assert.equal(firstQuery.posts_per_page, 2);
|
||||
assert.equal(firstQuery.no_found_rows, false);
|
||||
assert.deepEqual(firstQuery.category__in, [7]);
|
||||
assert.equal(fixture.aggregation_queries[1].category__in[0], 8);
|
||||
assert.equal(firstQuery.cat, '7');
|
||||
assert.equal(fixture.aggregation_queries[1].cat, '8');
|
||||
});
|
||||
|
||||
test('category filters keep dropdown and inline terms in separate rendered groups on the ajax-option pipeline', () => {
|
||||
|
||||
@@ -19,7 +19,12 @@ class WP_Query {
|
||||
public function __construct(public array $query_args = []) {
|
||||
$GLOBALS['discovery_queries'][] = $query_args;
|
||||
$cats = array_map('intval', (array) ($query_args['category__in'] ?? []));
|
||||
$rows = array_values(array_filter($GLOBALS['discovery_posts'], static fn(array $post): bool => !$cats || in_array($post['cat'], $cats, true)));
|
||||
$excluded = [];
|
||||
foreach (array_filter(explode(',', (string) ($query_args['cat'] ?? ''))) as $cat) {
|
||||
if ((int) $cat > 0) $cats[] = (int) $cat;
|
||||
if ((int) $cat < 0) $excluded[] = abs((int) $cat);
|
||||
}
|
||||
$rows = array_values(array_filter($GLOBALS['discovery_posts'], static fn(array $post): bool => (!$cats || in_array($post['cat'], $cats, true)) && !in_array($post['cat'], $excluded, true)));
|
||||
$this->found_posts = count($rows);
|
||||
$per_page = max(1, (int) ($query_args['posts_per_page'] ?? 10));
|
||||
$this->max_num_pages = (int) ceil($this->found_posts / $per_page);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const cases = JSON.parse(execFileSync(process.env.PHP_BINARY || 'php', [
|
||||
path.join(__dirname, 'tab-posts-render-harness.php'),
|
||||
], { encoding: 'utf8' })).cases;
|
||||
|
||||
for (const style of ['list', 'card', 'mini']) {
|
||||
test(`article ${style} widget renders its saved layout directly`, () => {
|
||||
const html = cases[`posts_${style}_detail`];
|
||||
assert.match(html, new RegExp(`class="widget-main-post mb20 style-${style}"`));
|
||||
assert.match(html, /data-widget="posts" data-instance="aksrt_widget_posts-3"/);
|
||||
assert.match(html, /data-loaded="1" aria-live="polite" aria-busy="false"/);
|
||||
assert.equal((html.match(/<h2 class="item-heading\b/g) || []).length, 4);
|
||||
assert.match(html, /data-widget-page="2"/);
|
||||
assert.match(html, /class="post_ajax_loader"/);
|
||||
assert.doesNotMatch(html, /post_ajax_loader is-loading|class="posts-row|ajax-next/);
|
||||
if (style === 'mini') assert.match(html, /class="zib-widget posts-mini-lists"/);
|
||||
});
|
||||
|
||||
test(`article ${style} widget reserves visible skeletons for lazy loading`, () => {
|
||||
const html = cases[`posts_${style}_ajax`];
|
||||
assert.match(html, new RegExp(`class="widget-main-post mb20 style-${style}"`));
|
||||
assert.match(html, /class="widget-ajaxpager lazyload" data-widget-lazy/);
|
||||
assert.match(html, /data-widget="posts" data-instance="aksrt_widget_posts-3"/);
|
||||
assert.match(html, /aria-live="polite" aria-busy="true"/);
|
||||
assert.match(html, /class="post_ajax_loader is-loading" role="status"/);
|
||||
assert.match(html, new RegExp(`data-placeholder="${style}:4"`));
|
||||
assert.doesNotMatch(html, /class="item-heading|ias-btn|data-widget-ias|post_ajax_trigger|lazyload-action=/);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
test('article list uses the reference widget container and keeps widget behavior classes', () => {
|
||||
const renderer = read('src/WidgetSystem/FrontendRenderer.php');
|
||||
const catalog = read('src/WidgetSystem/WidgetCatalog.php');
|
||||
assert.match(renderer, /\['user_card', 'author_card', 'tab_posts', 'posts'\]/);
|
||||
assert.match(renderer, /\$classes = \['zib-widget-wrap', 'aksrt-' \. str_replace/);
|
||||
assert.match(renderer, /echo '<div class="widget-container"><div>'/);
|
||||
assert.match(renderer, /WidgetVisibility::device_class\(\$instance\)/);
|
||||
assert.match(renderer, /!empty\(\$instance\['sidebar_affix'\]\)/);
|
||||
assert.match(renderer, /data-affix="true"/);
|
||||
assert.match(catalog, /in_array\(\$slug, \['buttons', 'notice', 'posts'\], true\)/);
|
||||
});
|
||||
|
||||
test('article list pagination shares the reference widget rules and obsolete trigger CSS is removed', () => {
|
||||
const css = read('assets/widgets/frontend.css');
|
||||
assert.match(css, /\.widget-tab-post \.pagenav, \.widget-main-post \.pagenav/);
|
||||
assert.match(css, /\.sidebar :is\(\.widget-tab-post, \.widget-main-post\) \.pagenav/);
|
||||
assert.match(css, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav a\.page-numbers/);
|
||||
assert.doesNotMatch(css, /\.post_ajax_trigger/);
|
||||
for (const token of [
|
||||
'.widget-tab-post .item-heading, .widget-main-post .item-heading',
|
||||
'.widget-tab-post .item-heading.font-bold, .widget-main-post .item-heading.font-bold',
|
||||
'.widget-tab-post .posts-mini, .widget-main-post .posts-mini',
|
||||
'.widget-tab-post .posts-mini-con, .widget-main-post .posts-mini-con',
|
||||
'.widget-tab-post .item-meta .meta-pay, .widget-main-post .item-meta .meta-pay',
|
||||
'.aksrt-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible',
|
||||
]) assert.ok(css.includes(token), `missing shared widget rule: ${token}`);
|
||||
});
|
||||
@@ -18,6 +18,12 @@ use AKSRT\WidgetSystem\Renderers\ContentRenderer;
|
||||
use AKSRT\WidgetSystem\Renderers\GeneralRenderer;
|
||||
use AKSRT\WidgetSystem\Renderers\SliderRenderer;
|
||||
|
||||
if (!function_exists('current_datetime')) {
|
||||
function current_datetime(): DateTimeImmutable {
|
||||
return new DateTimeImmutable('2026-06-15 12:30:00', new DateTimeZone('Asia/Shanghai'));
|
||||
}
|
||||
}
|
||||
|
||||
final class WidgetSystemTest {
|
||||
private int $assertions = 0;
|
||||
private array $failures = [];
|
||||
@@ -105,15 +111,24 @@ final class WidgetSystemTest {
|
||||
'count' => 6, 'cat' => '1, -2,3 bad',
|
||||
'orderby' => 'views', 'order' => 'DESC', 'limit_day' => 7, 'paginate' => '',
|
||||
], 3);
|
||||
$this->same([1, 3], $reference['category__in'], 'reference cat includes positive IDs');
|
||||
$this->same([2], $reference['category__not_in'], 'reference cat excludes negative IDs');
|
||||
$this->same('1,-2,3', $reference['cat'], 'reference cat preserves cleaned signed IDs for WP_Query descendant semantics');
|
||||
$this->false(isset($reference['category__in']) || isset($reference['category__not_in']), 'reference cat does not narrow matches to direct categories');
|
||||
$this->false(isset($reference['tax_query']), 'topic filters are removed with the topics feature');
|
||||
$this->same('aksrt_post_views', $reference['meta_key'], 'reference view ordering uses numeric view meta');
|
||||
$this->false(isset($reference['meta_query']), 'modern reference queries do not apply commerce filters by default');
|
||||
$this->same(1, $reference['paged'], 'no-pagination queries stay on first page');
|
||||
$this->same('2026-06-08 12:30:00', $reference['date_query'][0]['after'], 'limit_day subtracts days in the WordPress site timezone');
|
||||
$this->same('2026-06-15 12:30:00', $reference['date_query'][0]['before'], 'limit_day uses local WordPress time without a second offset conversion');
|
||||
$generic_categories = PostQuery::arguments(['category_ids' => '2,5', 'category_id' => 9], 1);
|
||||
$this->same([9], $generic_categories['category__in'], 'generic category_id continues to override category_ids');
|
||||
$this->false(isset($generic_categories['cat']), 'generic categories do not switch to modern cat syntax');
|
||||
$paywalled = PostQuery::arguments(['cat' => '1', 'zibpay_type' => ['1', '6'], 'paginate' => ''], 1, ['cat' => '1']);
|
||||
$this->contains('reading', json_encode($paywalled['meta_query'] ?? []), 'tab commerce filters map to pay meta');
|
||||
$this->contains('video', json_encode($paywalled['meta_query'] ?? []), 'tab commerce filters cover every selected type');
|
||||
$top_level_paywall = PostQuery::arguments(['cat' => '1', 'zibpay_type' => ['2'], 'paginate' => 'ajax'], 1);
|
||||
$this->contains('download', json_encode($top_level_paywall['meta_query'] ?? []), 'top-level reference commerce filters map to pay meta');
|
||||
$generic_paywall = PostQuery::arguments(['category_ids' => '1', 'pay_type' => 'gallery'], 1);
|
||||
$this->contains('gallery', json_encode($generic_paywall['meta_query'] ?? []), 'generic widget pay_type behavior remains available');
|
||||
$invalid = PostQuery::arguments(['cat' => '', 'orderby' => 'drop table'], 1, ['cat' => '']);
|
||||
$this->false(isset($invalid['meta_query']), 'modern reference fields never add commerce filters');
|
||||
$this->same('date', $invalid['orderby'], 'unknown orderby falls back');
|
||||
@@ -298,7 +313,7 @@ final class WidgetSystemTest {
|
||||
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1]));
|
||||
$this->error('aksrt_widget_instance_not_registered', $error, 'REST requires an assigned instance');
|
||||
$GLOBALS['aksrt_test_state']['sidebars'] = ['home_top_content' => ['aksrt_widget_posts-1']];
|
||||
update_option('widget_aksrt_widget_posts', [1 => ['title' => 'Saved only', 'count' => 2, 'style' => 'list'], '_multiwidget' => 1]);
|
||||
update_option('widget_aksrt_widget_posts', [1 => ['title' => 'Saved only', 'count' => 2, 'style' => 'list', 'paginate' => 'ajax'], '_multiwidget' => 1]);
|
||||
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 2, 'segment' => '']));
|
||||
$this->true($response instanceof WP_REST_Response, 'registered instance returns REST response');
|
||||
$data = $response->get_data();
|
||||
@@ -306,6 +321,8 @@ final class WidgetSystemTest {
|
||||
$this->same(2, $data['page'], 'REST page reaches saved-query pipeline');
|
||||
$this->contains('暂无内容', $data['html'], 'REST returns deterministic empty state');
|
||||
$this->same(4, end($GLOBALS['aksrt_test_state']['query_args'])['posts_per_page'], 'REST reloads saved normalized configuration');
|
||||
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1, 'rest_route' => '/aksrt/v1/widgets/posts/items']));
|
||||
$this->true($response instanceof WP_REST_Response, 'plain permalink transport parameter is accepted');
|
||||
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_hot_posts-1', 'page' => 1]));
|
||||
$this->error('aksrt_widget_invalid_instance', $error, 'REST binds instance ID to requested widget');
|
||||
}
|
||||
|
||||
@@ -33,15 +33,15 @@ test('un-normalized missing and invalid style use one shared mini resolution in
|
||||
}
|
||||
});
|
||||
|
||||
test('mini options render thumbnail, rank, author and meta on both pages with continuous numbering', () => {
|
||||
test('mini options render thumbnail, rank, author and meta; rank is limited to the first page', () => {
|
||||
const current = render().mini_all_options;
|
||||
assert.equal((current.first.match(/class="posts-mini ajax-item"/g) || []).length, 4);
|
||||
assert.match(current.first, /class="mr10"><div class="item-thumbnail"/);
|
||||
assert.match(current.first, /badg badg-sm mr3 c-red">1<\/span>/);
|
||||
assert.match(current.first, /class="meta-author flex ac"/);
|
||||
assert.match(current.first, /class="item-meta muted-2-color flex jsb ac/);
|
||||
assert.match(current.second, /badg badg-sm mr3 c-green">5<\/span>/);
|
||||
assert.match(current.second, /真实文章 36/);
|
||||
assert.doesNotMatch(current.second, /class="badg badg-sm/);
|
||||
assert.match(current.second, /真实文章 32/);
|
||||
assert.doesNotMatch(current.first + current.second, /class="posts-row|class="posts-item list|class="posts-item card/);
|
||||
});
|
||||
|
||||
@@ -152,12 +152,14 @@ test('multi-column frontend output uses the reference widget wrapper and carries
|
||||
assert.doesNotMatch(wrapper, /aksrt-widget__inner container|<aside>|<\/aside>/);
|
||||
|
||||
const frontend = read('src/WidgetSystem/FrontendRenderer.php');
|
||||
const routeStart = frontend.indexOf("if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts']");
|
||||
const routeStart = frontend.indexOf("if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts', 'posts']");
|
||||
const contentRouteStart = frontend.indexOf('if (in_array($definition->slug()', routeStart + 1);
|
||||
const referenceRoute = frontend.slice(routeStart, contentRouteStart);
|
||||
const contentRoute = frontend.slice(contentRouteStart, frontend.indexOf('private static function render_reference_widget'));
|
||||
assert.match(referenceRoute, /'user_card', 'author_card', 'tab_posts'/);
|
||||
assert.doesNotMatch(contentRoute, /'tab_posts'/);
|
||||
assert.match(referenceRoute, /'user_card', 'author_card', 'tab_posts', 'posts'/);
|
||||
assert.doesNotMatch(contentRoute, /'tab_posts'|'posts'/);
|
||||
assert.match(frontend, /\$affix_attr = !empty\(\$instance\['sidebar_affix'\]\) \? ' data-affix="true"'/);
|
||||
assert.match(frontend, /\. \$affix_attr \. \(\$style/);
|
||||
});
|
||||
|
||||
test('widget list and card CSS is included in the public page bundle at desktop and mobile breakpoints', () => {
|
||||
|
||||
@@ -31,6 +31,10 @@ class WP_Query {
|
||||
$rows = array_values(array_filter($GLOBALS['tab_fixture_posts'], static function (array $post) use ($args): bool {
|
||||
$include = array_map('intval', (array) ($args['category__in'] ?? []));
|
||||
$exclude = array_map('intval', (array) ($args['category__not_in'] ?? []));
|
||||
foreach (array_filter(explode(',', (string) ($args['cat'] ?? ''))) as $cat) {
|
||||
if ((int) $cat > 0) $include[] = (int) $cat;
|
||||
if ((int) $cat < 0) $exclude[] = abs((int) $cat);
|
||||
}
|
||||
return (!$include || in_array($post['cat'], $include, true)) && !in_array($post['cat'], $exclude, true);
|
||||
}));
|
||||
$orderby = (string) ($args['orderby'] ?? 'date');
|
||||
@@ -239,7 +243,7 @@ namespace {
|
||||
'avatar_calls' => array_slice($GLOBALS['tab_fixture_avatar_calls'], $avatar_start),
|
||||
];
|
||||
}
|
||||
$mini_instance = ['style' => 'mini', 'mini_opt' => ['show_thumb', 'show_number', 'show_meta'], 'count' => 4, 'paginate' => '', 'tabs' => $tabs];
|
||||
$mini_instance = ['style' => 'mini', 'mini_opt' => ['show_thumb', 'show_number', 'show_meta'], 'count' => 4, 'paginate' => 'number', 'tabs' => $tabs];
|
||||
$mini_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_instance);
|
||||
$avatar_start = count($GLOBALS['tab_fixture_avatar_calls']);
|
||||
$mini_first = \AKSRT\WidgetSystem\PostRenderer::payload($mini_instance, 'tab_posts', 1, '0')['html'];
|
||||
@@ -249,6 +253,14 @@ namespace {
|
||||
'second' => $mini_second,
|
||||
'avatar_calls' => array_slice($GLOBALS['tab_fixture_avatar_calls'], $avatar_start),
|
||||
];
|
||||
$cases['no_pagination_page'] = \AKSRT\WidgetSystem\PostRenderer::payload(array_merge($mini_instance, ['paginate' => '']), 'tab_posts', 2, '0');
|
||||
foreach (['list', 'card', 'mini'] as $posts_style) {
|
||||
$posts_definition = \AKSRT\WidgetSystem\WidgetRegistry::get('posts');
|
||||
foreach (['detail', 'ajax'] as $load_mode) {
|
||||
$posts_instance = $posts_definition->normalize(['style' => $posts_style, 'load_mode' => $load_mode, 'count' => 4, 'paginate' => 'number', 'title' => '文章列表']);
|
||||
$cases['posts_' . $posts_style . '_' . $load_mode] = \AKSRT\WidgetSystem\Renderers\ContentRenderer::render('posts', $posts_instance, ['widget_id' => 'aksrt_widget_posts-3']);
|
||||
}
|
||||
}
|
||||
$GLOBALS['tab_fixture_global_style']['post_list_author'] = false;
|
||||
$mini_time_instance = ['style' => 'mini', 'mini_opt' => ['show_meta'], 'count' => 4, 'paginate' => '', 'tabs' => $tabs];
|
||||
$mini_time_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_time_instance);
|
||||
|
||||
@@ -332,9 +332,25 @@ function addJumpControl(document, pager, { max = 20, value = '4' } = {}) {
|
||||
return { input, button };
|
||||
}
|
||||
|
||||
function createRuntime({ fetch, pages } = {}) {
|
||||
function postsMarkup(document, unveiled = false) {
|
||||
const root = document.createElement('div');
|
||||
root.className = 'widget-main-post style-list';
|
||||
const pager = document.createElement('div');
|
||||
pager.className = 'widget-ajaxpager ' + (unveiled ? 'lazyloaded' : 'lazyload');
|
||||
pager.setAttribute('data-widget-items', '');
|
||||
pager.setAttribute('data-widget-lazy', '');
|
||||
pager.setAttribute('data-widget', 'posts');
|
||||
pager.setAttribute('data-instance', 'aksrt_widget_posts-2');
|
||||
pager.setAttribute('data-page', '1');
|
||||
pager.setAttribute('aria-busy', 'true');
|
||||
pager.innerHTML = '<div class="post_ajax_loader is-loading"><div class="posts-item placeholder">Loading</div></div>';
|
||||
root.appendChild(pager);
|
||||
return { root };
|
||||
}
|
||||
|
||||
function createRuntime({ fetch, pages, posts = false, unveiled = false, restUrl = '/wp-json/aksrt/v1/widgets/', observe = false } = {}) {
|
||||
const document = new MiniDocument();
|
||||
const { root: widget, nav, tabOne, tabTwo } = tabMarkup(document);
|
||||
const { root: widget, nav, tabOne, tabTwo } = posts ? postsMarkup(document, unveiled) : tabMarkup(document);
|
||||
document.body.appendChild(widget);
|
||||
const autoFun = [];
|
||||
function jQuery() {
|
||||
@@ -344,7 +360,7 @@ function createRuntime({ fetch, pages } = {}) {
|
||||
};
|
||||
}
|
||||
const requests = [];
|
||||
const config = { restUrl: '/wp-json/aksrt/v1', i18n: { error: 'error', retry: 'retry' } };
|
||||
const config = { restUrl, i18n: { error: 'error', retry: 'retry' } };
|
||||
const fetchStub = (url) => {
|
||||
requests.push(url);
|
||||
if (fetch) return fetch(url);
|
||||
@@ -357,11 +373,18 @@ function createRuntime({ fetch, pages } = {}) {
|
||||
};
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(response) });
|
||||
};
|
||||
let notifyMutation;
|
||||
const Observer = observe ? class {
|
||||
constructor(callback) { notifyMutation = callback; }
|
||||
observe() {}
|
||||
} : undefined;
|
||||
const window = {
|
||||
aksrtWidgets: config,
|
||||
location: { href: 'https://example.test/' },
|
||||
matchMedia: () => ({ matches: false }),
|
||||
jQuery,
|
||||
};
|
||||
if (observe) window.MutationObserver = Observer;
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root, 'assets/widgets/frontend.js'), 'utf8'), {
|
||||
window,
|
||||
document,
|
||||
@@ -373,9 +396,10 @@ function createRuntime({ fetch, pages } = {}) {
|
||||
Object,
|
||||
Math,
|
||||
encodeURIComponent,
|
||||
MutationObserver: undefined,
|
||||
URL,
|
||||
MutationObserver: Observer,
|
||||
});
|
||||
return { document, widget, nav, tabOne, tabTwo, requests, autoFun };
|
||||
return { document, widget, nav, tabOne, tabTwo, requests, autoFun, notifyMutation };
|
||||
}
|
||||
|
||||
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
||||
@@ -677,3 +701,76 @@ test('jump input clamps pages to max and rejects invalid or current pages withou
|
||||
assert.equal(runtime.requests.length, 0, 'submitting the already selected page does not request again');
|
||||
assert.equal(runtime.document.activeElement, input, 'invalid and current page values return focus to the input');
|
||||
});
|
||||
|
||||
test('article widget waits for its own lazy reveal and fetches the first page once', async () => {
|
||||
const transport = pendingFetch();
|
||||
const runtime = createRuntime({ posts: true, fetch: transport.fetch });
|
||||
const pager = runtime.widget.querySelector('[data-widget-items]');
|
||||
assert.equal(runtime.requests.length, 0, 'offscreen widgets keep their placeholder without fetching');
|
||||
assert.ok(pager.querySelector('.post_ajax_loader.is-loading'));
|
||||
pager.querySelector('.placeholder').dispatch('lazybeforeunveil');
|
||||
assert.equal(runtime.requests.length, 0, 'a descendant image reveal does not load the pager');
|
||||
pager.dispatch('lazybeforeunveil');
|
||||
pager.dispatch('lazybeforeunveil');
|
||||
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
||||
const url = new URL(runtime.requests[0]);
|
||||
assert.equal(url.pathname, '/wp-json/aksrt/v1/widgets/posts/items');
|
||||
assert.equal(url.searchParams.get('instance'), 'aksrt_widget_posts-2');
|
||||
transport.pending[0].resolve({ html: '<posts class="posts-item list ajax-item" data-post-id="first">First article</posts>', page: 1, paginate_html: '' });
|
||||
await tick();
|
||||
assert.ok(pager.querySelector('[data-post-id="first"]'));
|
||||
assert.equal(pager.dataset.loaded, '1');
|
||||
assert.equal(pager.getAttribute('aria-busy'), 'false');
|
||||
assert.equal(pager.querySelector('.post_ajax_loader').classList.contains('is-loading'), false);
|
||||
pager.dispatch('lazybeforeunveil');
|
||||
assert.equal(runtime.requests.length, 1, 'a completed first page is never fetched again by lazySizes');
|
||||
});
|
||||
|
||||
test('article widget loads when lazySizes unveiled it before the widget client initialized', async () => {
|
||||
const runtime = createRuntime({ posts: true, unveiled: true });
|
||||
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
||||
await tick();
|
||||
const pager = runtime.widget.querySelector('[data-widget-items]');
|
||||
assert.equal(pager.dataset.loaded, '1');
|
||||
assert.ok(pager.querySelector('[data-post-id]'));
|
||||
});
|
||||
|
||||
test('article widget failure exposes a retry that replaces the initial skeleton', async () => {
|
||||
const transport = pendingFetch();
|
||||
const runtime = createRuntime({ posts: true, fetch: transport.fetch });
|
||||
const pager = runtime.widget.querySelector('[data-widget-items]');
|
||||
pager.dispatch('lazybeforeunveil');
|
||||
transport.pending[0].reject(new Error('offline'));
|
||||
await tick();
|
||||
assert.equal(pager.getAttribute('aria-busy'), 'false');
|
||||
assert.equal(pager.querySelector('.post_ajax_loader').classList.contains('is-loading'), false);
|
||||
pager.querySelector('[data-widget-retry]').click();
|
||||
assert.deepEqual(pageRequests(runtime.requests), [1, 1]);
|
||||
transport.pending[1].resolve({ html: '<posts class="posts-item card ajax-item" data-post-id="retried">Retried</posts>', page: 1, paginate_html: '' });
|
||||
await tick();
|
||||
assert.ok(pager.querySelector('[data-post-id="retried"]'));
|
||||
assert.equal(pager.querySelector('[data-widget-retry]'), null);
|
||||
});
|
||||
|
||||
test('article REST addresses preserve the route for plain WordPress permalinks', async () => {
|
||||
const runtime = createRuntime({ posts: true, unveiled: true, restUrl: 'https://example.test/blog/?rest_route=/aksrt/v1/widgets/' });
|
||||
const url = new URL(runtime.requests[0]);
|
||||
assert.equal(url.pathname, '/blog/');
|
||||
assert.equal(url.searchParams.get('rest_route'), '/aksrt/v1/widgets/posts/items');
|
||||
assert.equal(url.searchParams.get('instance'), 'aksrt_widget_posts-2');
|
||||
assert.equal(url.searchParams.get('page'), '1');
|
||||
await tick();
|
||||
assert.equal(runtime.widget.querySelector('[data-widget-items]').dataset.loaded, '1');
|
||||
});
|
||||
|
||||
test('a pager inserted as the mutation root still receives its lazy listener', async () => {
|
||||
const runtime = createRuntime({ observe: true });
|
||||
const pager = postsMarkup(runtime.document).root.querySelector('[data-widget-items]');
|
||||
pager.nodeType = 1;
|
||||
runtime.document.body.appendChild(pager);
|
||||
runtime.notifyMutation([{ addedNodes: [pager] }]);
|
||||
pager.dispatch('lazybeforeunveil');
|
||||
await tick();
|
||||
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
||||
assert.equal(pager.dataset.loaded, '1');
|
||||
});
|
||||
@@ -20,7 +20,7 @@ function declarations(selector) {
|
||||
return result;
|
||||
}
|
||||
|
||||
test('tab post titles restore reference weight after public typography while preserving the bold option', () => {
|
||||
test('tab and single post widget titles restore reference weight while preserving the bold option', () => {
|
||||
// public.css sets h2 to 700. The widget must override that selector;
|
||||
// the explicit global heading-bold option must still win inside it.
|
||||
assert.match(read('assets/css/core/typography.css'), /font-weight:\s*700/);
|
||||
@@ -28,12 +28,19 @@ test('tab post titles restore reference weight after public typography while pre
|
||||
assert.equal(heading['font-weight'], '500');
|
||||
assert.equal(heading.color, 'inherit');
|
||||
assert.equal(declarations('.widget-tab-post .item-heading.font-bold')['font-weight'], '700');
|
||||
assert.equal(declarations('.widget-main-post .item-heading')['font-weight'], '500');
|
||||
assert.equal(declarations('.widget-main-post .item-heading.font-bold')['font-weight'], '700');
|
||||
const shell = declarations('.aksrt-tab-posts-wrap');
|
||||
assert.equal(shell['font-size'], '14px');
|
||||
assert.equal(shell['line-height'], '1.42857143');
|
||||
assert.equal(shell.color, 'var(--main-color,#4e5358)');
|
||||
assert.equal(declarations('.widget-tab-post .posts-mini')['vertical-align'], 'baseline');
|
||||
assert.equal(declarations('.widget-tab-post .posts-mini-con').flex, 'auto');
|
||||
assert.equal(declarations('.widget-main-post .posts-mini')['vertical-align'], 'baseline');
|
||||
assert.equal(declarations('.widget-main-post .posts-mini-con').flex, 'auto');
|
||||
assert.equal(declarations('.widget-tab-post .item-meta .meta-pay').cursor, 'auto');
|
||||
assert.equal(declarations('.widget-main-post .item-meta .meta-pay').cursor, 'auto');
|
||||
assert.match(css, /\.aksrt-posts-wrap :where\(a, button, input, select, textarea, \[tabindex\]\):focus-visible/);
|
||||
});
|
||||
|
||||
test('widget pagination restores compact reference metrics over public pagination overrides', () => {
|
||||
@@ -52,6 +59,10 @@ test('widget pagination restores compact reference metrics over public paginatio
|
||||
assert.equal(nestedLabel['min-width'], '0');
|
||||
assert.equal(nestedLabel.padding, '0');
|
||||
const hover = declarations('.widget-tab-post .pagenav a.page-numbers:hover');
|
||||
assert.equal(declarations('.widget-main-post .pagenav').display, 'block');
|
||||
assert.equal(declarations('.widget-main-post .pagenav .page-numbers').padding, '6px 9px 5px');
|
||||
assert.equal(declarations('.widget-main-post .pagenav a.page-numbers:hover').opacity, '.7');
|
||||
assert.match(read('assets/css/main.css'), /\.pagenav \.current\s*\{[^}]*background-color:\s*var\(--focus-color\)\s*!important;[^}]*color:\s*#fff\s*!important;/s);
|
||||
assert.equal(hover.background, 'var(--main-border-color)');
|
||||
assert.equal(hover.opacity, '.7');
|
||||
});
|
||||
@@ -61,13 +72,15 @@ test('jump control keeps the reference inline layout and responsive numeric link
|
||||
assert.equal(jump.width, 'auto', 'public mobile CSS must not stretch the jump control to a full row');
|
||||
assert.equal(jump.display, 'inline-flex');
|
||||
const input = declarations('.widget-tab-post .pagenav .pag-jump .jump-input');
|
||||
assert.equal(declarations('.widget-main-post .pagenav .pag-jump').display, 'inline-flex');
|
||||
assert.equal(declarations('.widget-main-post .pagenav .pag-jump .jump-input').position, 'absolute');
|
||||
assert.equal(input.position, 'absolute');
|
||||
assert.equal(input.width, '100%');
|
||||
assert.equal(input.opacity, '0');
|
||||
const mobile = css.slice(css.indexOf('@media (max-width:640px)'));
|
||||
assert.match(mobile, /\.widget-tab-post \.pagenav a\.page-numbers\s*\{\s*display:none/);
|
||||
assert.match(mobile, /\.widget-tab-post \.pagenav a\.prev\s*\{\s*display:inline-block/);
|
||||
assert.match(mobile, /\.widget-tab-post \.pagenav \.page-numbers\s*\{\s*min-width:28px;\s*margin:2px;\s*font-size:12px/);
|
||||
assert.match(mobile, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav a\.page-numbers\s*\{\s*display:none/);
|
||||
assert.match(mobile, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav a\.prev\s*\{\s*display:inline-block/);
|
||||
assert.match(mobile, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav \.page-numbers\s*\{\s*min-width:28px;\s*margin:2px;\s*font-size:12px/);
|
||||
});
|
||||
|
||||
test('modern tabs have one interaction owner and no retired tab selector or unused widget-style field', () => {
|
||||
|
||||
@@ -20,8 +20,8 @@ test('modern widget schemas expose Zibll field names and defaults', () => {
|
||||
test('tab and slider renderers retain reference DOM contracts', () => {
|
||||
const content = read('src/WidgetSystem/Renderers/ContentRenderer.php');
|
||||
const slider = read('src/WidgetSystem/Renderers/SliderRenderer.php');
|
||||
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'ias-btn', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
assert.doesNotMatch(content, /post_ajax_trigger/);
|
||||
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'data-widget-lazy', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
assert.doesNotMatch(content, /post_ajax_trigger|ias-btn|data-widget-ias/);
|
||||
for (const token of ['relative zib-slider', 'new-swiper slide-widget', 'swiper-wrapper', 'swiper-slide', 'slide-layer', 'data-swiper-parallax', 'swiper-button-prev', 'swiper-pagination', 'widget-slider-graphic', 'slider-col', 'cover-col', 'widget-graphic-cover']) assert.match(slider, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user