feat(widgets): add phase 4 validation and accessibility
This commit is contained in:
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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); } }
|
||||
|
||||
@@ -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; });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user