feat(widgets): add phase 4 validation and accessibility

This commit is contained in:
2026-08-17 11:40:50 +00:00
parent 8da6439675
commit be1979d378
23 changed files with 8078 additions and 53 deletions
+44
View File
@@ -0,0 +1,44 @@
(function (root, factory) {
'use strict';
var api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
else root.AKSRTWidgetAdminCore = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
function parseDependency(raw) {
if (Array.isArray(raw)) return raw;
try {
var parsed = JSON.parse(String(raw || '[]'));
return Array.isArray(parsed) ? parsed : [];
} catch (_error) {
return [];
}
}
function dependencyVisible(rule, current) {
rule = parseDependency(rule);
if (rule.length < 3) return true;
var expected = String(rule[2]);
var actual = String(current == null ? '' : current);
return rule[1] === '==' ? actual === expected : actual !== expected;
}
function replaceRepeaterIndexes(name, rowIndexes) {
var foundWidgetNumber = false;
var cursor = 0;
return String(name || '').replace(/\[(\d+|__index_[a-f0-9]+__)\]/g, function (match, index) {
if (!foundWidgetNumber && /^\d+$/.test(index)) {
foundWidgetNumber = true;
return match;
}
return cursor < rowIndexes.length ? '[' + Number(rowIndexes[cursor++]) + ']' : match;
});
}
return {
parseDependency: parseDependency,
dependencyVisible: dependencyVisible,
replaceRepeaterIndexes: replaceRepeaterIndexes
};
});
+4 -15
View File
@@ -2,6 +2,7 @@
'use strict';
const config = window.aksrtWidgetAdmin || {};
const core = window.AKSRTWidgetAdminCore;
function initForm(root) {
const $root = $(root);
@@ -68,15 +69,7 @@
$form.find('[name]').each(function () {
const rowIndexes = $(this).parents('.aksrt-widget-repeater__row').get().reverse().map((row) => Number(row.dataset.rowIndex || 0));
if (!rowIndexes.length) return;
let foundWidgetNumber = false;
let cursor = 0;
this.name = this.name.replace(/\[(\d+|__index_[a-f0-9]+__)\]/g, (match, index) => {
if (!foundWidgetNumber && /^\d+$/.test(index)) {
foundWidgetNumber = true;
return match;
}
return cursor < rowIndexes.length ? `[${rowIndexes[cursor++]}]` : match;
});
this.name = core.replaceRepeaterIndexes(this.name, rowIndexes);
});
}
@@ -89,14 +82,10 @@
function evaluateDependencies($form) {
$form.find('[data-dependency]').each(function () {
let rule;
try { rule = JSON.parse(this.dataset.dependency || '[]'); } catch (_error) { return; }
if (!Array.isArray(rule) || rule.length < 3) return;
const rule = core.parseDependency(this.dataset.dependency || '[]');
const $row = $(this).closest('.aksrt-widget-repeater__row');
const current = fieldValue($row.length ? $row : $form, String(rule[0]));
const expected = String(rule[2]);
const visible = rule[1] === '==' ? current === expected : current !== expected;
this.hidden = !visible;
this.hidden = !core.dependencyVisible(rule, current);
});
}
+7
View File
@@ -15,6 +15,9 @@
.aksrt-widget img, .aksrt-widget video { max-width: 100%; }
.aksrt-widget a { color: inherit; text-decoration: none; }
.aksrt-widget button, .aksrt-widget input { font: inherit; }
.aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline: 3px solid var(--focus-color, #ff5473) !important; outline-offset: 3px; }
.dark-theme .aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible,
[data-theme="dark"] .aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline-color: #ffd166 !important; }
.aksrt-widget__header { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 16px; }
.aksrt-widget__header--big { display: block; text-align: center; margin-bottom: 30px; }
.aksrt-widget__title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 700; }
@@ -96,6 +99,7 @@
.aksrt-widget-comments { display: grid; gap: 15px; margin: 0; padding: 0; list-style: none; }.aksrt-widget-comments li { display: flex; gap: 11px; }.aksrt-widget-comments__avatar img { width: 42px; height: 42px; border-radius: 50%; }.aksrt-widget-comments p { margin: 3px 0; color: var(--muted-color,#8a8c98); font-size: 13px; line-height: 1.55; }.aksrt-widget-comments__source { font-size: 12px; opacity: .65; }
.aksrt-widget-links { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; }.aksrt-widget-links a { display: flex; flex-direction: column; gap: 4px; padding: 14px; border-radius: 9px; background: var(--main-bg,#fff); box-shadow: 0 6px 20px rgba(31,35,48,.06); }.aksrt-widget-links small { color: var(--muted-color,#8a8c98); }.aksrt-widget-links--simple { display: flex; flex-wrap: wrap; }.aksrt-widget-links--simple a { padding: 5px 9px; box-shadow: none; background: transparent; }
.aksrt-widget-notice { display: flex; height: 48px; align-items: center; gap: 12px; padding: 0 16px; overflow: hidden; border-radius: 9px; background: var(--main-bg,#fff); box-shadow: 0 7px 22px rgba(31,35,48,.06); }.aksrt-widget-notice .swiper { height: 48px; flex: 1; }.aksrt-widget-notice .swiper-slide, .aksrt-widget-notice .swiper-slide a { display: flex; height: 48px; align-items: center; gap: 10px; }.aksrt-widget-notice .swiper-slide span { overflow: hidden; color: var(--muted-color,#8a8c98); text-overflow: ellipsis; white-space: nowrap; }
.aksrt-widget-swiper { position: relative; }.aksrt-widget-swiper__pause { position: absolute; z-index: 6; top: 10px; right: 10px; display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center; border: 1px solid rgba(255,255,255,.45); border-radius: 50%; background: rgba(20,21,27,.72); color: #fff; cursor: pointer; }.aksrt-widget-notice .aksrt-widget-swiper__pause { top: 7px; right: 4px; width: 30px; height: 30px; }
.aksrt-term-card { display: block; overflow: hidden; border-radius: var(--aksrt-widget-radius); background: var(--main-bg,#fff); box-shadow: 0 8px 28px rgba(31,35,48,.07); }.aksrt-term-card__media { display: block; aspect-ratio: 16/9; background: linear-gradient(135deg,#667eea,#764ba2); }.aksrt-term-card__media img { width: 100%; height: 100%; object-fit: cover; }.aksrt-term-card__body { display: flex; flex-direction: column; gap: 5px; padding: 14px; }.aksrt-term-card__body small { min-height: 38px; color: var(--muted-color,#8a8c98); }.aksrt-term-card__body em { font-size: 12px; color: var(--focus-color,#ff5473); font-style: normal; }
.aksrt-term-aggregation { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; }.aksrt-term-aggregation__card { padding: 16px; border-radius: var(--aksrt-widget-radius); background: var(--main-bg,#fff); box-shadow: 0 8px 28px rgba(31,35,48,.07); }.aksrt-term-aggregation header a { display: flex; justify-content: space-between; }.aksrt-term-aggregation header span { color: var(--muted-color,#8a8c98); font-size: 13px; }.aksrt-term-aggregation ul { display: grid; gap: 10px; margin: 14px 0 0; padding: 0; list-style: none; }.aksrt-term-aggregation li a { display: flex; align-items: center; gap: 10px; }.aksrt-term-aggregation li img { width: 55px; height: 42px; border-radius: 6px; object-fit: cover; }
@@ -131,5 +135,8 @@
@media (prefers-reduced-motion: reduce) {
.aksrt-widget *, .aksrt-widget *::before, .aksrt-widget *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
@media (forced-colors: active) {
.aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline-color: Highlight !important; }
}
.aksrt-widget--animate { animation: aksrt-widget-fade .55s both; }.aksrt-widget--animate-slideup { animation-name: aksrt-widget-slideup; }.aksrt-widget--animate-slidedown { animation-name: aksrt-widget-slidedown; }.aksrt-widget--animate-slideright { animation-name: aksrt-widget-slideright; }.aksrt-widget--animate-slideleft { animation-name: aksrt-widget-slideleft; }.aksrt-widget--animate-zoomin { animation-name: aksrt-widget-zoomin; }.aksrt-widget--animate-zoomout { animation-name: aksrt-widget-zoomout; }.aksrt-widget--animate-repeat { animation-iteration-count: infinite; animation-duration: 2s; }
@keyframes aksrt-widget-fade { from { opacity: 0; } }@keyframes aksrt-widget-slideup { from { opacity:0;transform:translateY(25px); } }@keyframes aksrt-widget-slidedown { from { opacity:0;transform:translateY(-25px); } }@keyframes aksrt-widget-slideright { from { opacity:0;transform:translateX(-25px); } }@keyframes aksrt-widget-slideleft { from { opacity:0;transform:translateX(25px); } }@keyframes aksrt-widget-zoomin { from { opacity:0;transform:scale(.9); } }@keyframes aksrt-widget-zoomout { from { opacity:0;transform:scale(1.1); } }
+73 -4
View File
@@ -11,6 +11,59 @@
return true;
}
function moveTabFocus(event, root) {
var current = event.target.closest('[role="tab"]');
if (!current) return;
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
var index = tabs.indexOf(current);
var next = index;
if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
else if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = tabs.length - 1;
else return;
event.preventDefault();
tabs[next].focus();
tabs[next].click();
}
function initSwiperPause(el, swiper) {
if (!swiper.autoplay) return;
var i18n = config.i18n || {};
var manuallyPaused = false;
var button = document.createElement('button');
button.type = 'button';
button.className = 'aksrt-widget-swiper__pause';
button.setAttribute('aria-pressed', 'false');
function pause() {
if (typeof swiper.autoplay.pause === 'function') swiper.autoplay.pause();
else if (typeof swiper.autoplay.stop === 'function') swiper.autoplay.stop();
}
function resume() {
if (typeof swiper.autoplay.resume === 'function') swiper.autoplay.resume();
else if (typeof swiper.autoplay.start === 'function') swiper.autoplay.start();
}
function updateButton() {
button.setAttribute('aria-pressed', manuallyPaused ? 'true' : 'false');
button.setAttribute('aria-label', manuallyPaused ? (i18n.resume || '继续自动播放') : (i18n.pause || '暂停自动播放'));
button.textContent = manuallyPaused ? '▶' : 'Ⅱ';
}
button.addEventListener('click', function () {
manuallyPaused = !manuallyPaused;
if (manuallyPaused) pause();
else resume();
updateButton();
});
el.addEventListener('focusin', pause);
el.addEventListener('focusout', function (event) {
if (!manuallyPaused && !el.contains(event.relatedTarget)) resume();
});
updateButton();
el.appendChild(button);
}
function initSwiper(el) {
if (!once(el, 'Swiper') || typeof window.Swiper !== 'function') return;
var kind = el.dataset.aksrtSwiper || 'cards';
@@ -37,11 +90,13 @@
options.effect = el.dataset.effect || 'slide';
options.parallax = !!el.querySelector('[data-swiper-parallax]');
}
if (!reducedMotion && (el.dataset.autoplay === '1' || kind === 'notice')) {
var shouldAutoplay = !reducedMotion && (el.dataset.autoplay === '1' || kind === 'notice');
if (shouldAutoplay) {
options.autoplay = { delay: Number(el.dataset.interval || 4000), disableOnInteraction: false, pauseOnMouseEnter: true };
}
try {
el.aksrtSwiper = new window.Swiper(el, options);
if (shouldAutoplay) initSwiperPause(el, el.aksrtSwiper);
} catch (error) {
el.classList.add('is-fallback');
}
@@ -161,6 +216,7 @@
if (!widget || !instance || !config.restUrl || container.dataset.loading === '1') return;
container.dataset.loading = '1';
container.classList.add('is-loading');
container.setAttribute('aria-busy', 'true');
fetch(itemsUrl(widget, instance, page, segment), { credentials: 'same-origin', headers: { Accept: 'application/json' } })
.then(function (response) { if (!response.ok) throw new Error('items'); return response.json(); })
.then(function (data) {
@@ -180,7 +236,11 @@
scan(container);
})
.catch(function () { container.innerHTML = errorMarkup(); })
.finally(function () { delete container.dataset.loading; container.classList.remove('is-loading'); });
.finally(function () {
delete container.dataset.loading;
container.classList.remove('is-loading');
container.setAttribute('aria-busy', 'false');
});
}
function initItems(container) {
@@ -207,12 +267,17 @@
function initTabs(tabs) {
if (!once(tabs, 'Tabs')) return;
tabs.addEventListener('keydown', function (event) { moveTabFocus(event, tabs); });
tabs.addEventListener('click', function (event) {
var button = event.target.closest('[data-segment]');
if (!button) return;
tabs.querySelectorAll('[role="tab"]').forEach(function (tab) { tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); });
tabs.querySelectorAll('[role="tab"]').forEach(function (tab) {
tab.setAttribute('aria-selected', tab === button ? 'true' : 'false');
tab.setAttribute('tabindex', tab === button ? '0' : '-1');
});
var panel = tabs.querySelector('[data-widget-items]');
panel.dataset.segment = button.dataset.segment;
panel.setAttribute('aria-labelledby', button.id);
loadItems(panel, 1, false);
});
}
@@ -236,11 +301,15 @@
function initFeatures(features) {
if (!once(features, 'Features')) return;
features.addEventListener('keydown', function (event) { moveTabFocus(event, features); });
features.addEventListener('click', function (event) {
var button = event.target.closest('[data-feature-index]');
if (!button) return;
var index = button.dataset.featureIndex;
features.querySelectorAll('[data-feature-index]').forEach(function (tab) { tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); });
features.querySelectorAll('[data-feature-index]').forEach(function (tab) {
tab.setAttribute('aria-selected', tab === button ? 'true' : 'false');
tab.setAttribute('tabindex', tab === button ? '0' : '-1');
});
features.querySelectorAll('[data-feature-panel]').forEach(function (panel) { panel.hidden = panel.dataset.featurePanel !== index; });
});
}