fix: резервные форматы примеров и stage snapshot (#16)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
deploy-preland / deploy zoomstage.online (push) Successful in 42s
This commit is contained in:
1 parent
0e6611da73
commit
f87bef2443
33 files changed
+199
-18
No files matched your search
@@ -2,6 +2,8 @@
|
||||
/** Общая inline-логика Index/Preland: синхронная копия в обоих репозиториях. */
|
||||
return <<<'JS'
|
||||
window.ZoomzillaExamples = {
|
||||
// Маленькие dataURI (AVIF 16×16, WebP 1×1) проверяют декодер один раз, без сетевой пробы каждого файла.
|
||||
codecSupport: new Map(),
|
||||
attach(mount) {
|
||||
const root = document.getElementById('examples');
|
||||
if (!root) return;
|
||||
@@ -67,20 +69,47 @@ window.ZoomzillaExamples = {
|
||||
return promise;
|
||||
}
|
||||
|
||||
const codecProbes = {"avif":"AAAAHGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZgAAAOptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAAAAAAAA5waXRtAAAAAAABAAAAImlsb2MAAAAAREAAAQABAAAAAAEOAAEAAAAAAAAAHgAAACNpaW5mAAAAAAABAAAAFWluZmUCAAAAAAEAAGF2MDEAAAAAamlwcnAAAABLaXBjbwAAABNjb2xybmNseAACAAIABoAAAAAMYXYxQ4EADAAAAAAUaXNwZQAAAAAAAAAQAAAAEAAAABBwaXhpAAAAAAMICAgAAAAXaXBtYQAAAAAAAAABAAEEgYIDhAAAACZtZGF0EgAKBhgM/9oQgDISF/AAAEgAAACpjltftKtLwWis","webp":"UklGRjwAAABXRUJQVlA4IDAAAADQAQCdASoBAAEAAgA0JaACdLoB+AADsAD+8MQL/yC5YXXI1/8gP+QH/ID/+PIAAAA="};
|
||||
function supportsCodec(url) {
|
||||
const match = /\.(avif|webp)(?:[?#]|$)/i.exec(url);
|
||||
const format = match ? match[1].toLowerCase() : null;
|
||||
if (!format) return Promise.resolve(true);
|
||||
const cache = window.ZoomzillaExamples.codecSupport;
|
||||
if (!cache.has(format)) cache.set(format, new Promise(resolve => {
|
||||
const image = new Image();
|
||||
const timer = window.setTimeout(() => finish(false), 1000);
|
||||
let finished = false;
|
||||
function finish(supported) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
window.clearTimeout(timer);
|
||||
image.onload = image.onerror = null;
|
||||
resolve(supported);
|
||||
}
|
||||
image.onerror = () => finish(false);
|
||||
image.onload = async () => {
|
||||
try {
|
||||
if (image.decode) await image.decode();
|
||||
finish(image.naturalWidth === (format === 'avif' ? 16 : 1));
|
||||
} catch (_) { finish(false); }
|
||||
};
|
||||
image.src = 'data:image/' + format + ';base64,' + codecProbes[format];
|
||||
}));
|
||||
return cache.get(format);
|
||||
}
|
||||
async function loadCandidates(item, role, variant, priority) {
|
||||
const suffix = variant === 'mob' ? 'Mob' : '';
|
||||
const candidates = [item.dataset[role + suffix]];
|
||||
if (role === 'thumb') {
|
||||
for (const format of ['Webp', 'Jpg']) {
|
||||
const fallback = item.dataset['thumb' + suffix + format];
|
||||
if (fallback) candidates.push(fallback);
|
||||
}
|
||||
for (const format of ['Webp', 'Jpg']) {
|
||||
const fallback = item.dataset[role + suffix + format];
|
||||
if (fallback && !candidates.includes(fallback)) candidates.push(fallback);
|
||||
}
|
||||
let failure;
|
||||
for (const url of candidates) {
|
||||
if (!await supportsCodec(url)) continue;
|
||||
try { return await loadImage(url, priority); } catch (error) { failure = error; }
|
||||
}
|
||||
throw failure;
|
||||
throw failure || new Error('example_codec_unavailable');
|
||||
}
|
||||
|
||||
async function preparePair(item, variant, priority = 'low') {
|
||||
@@ -112,6 +141,35 @@ window.ZoomzillaExamples = {
|
||||
}
|
||||
}
|
||||
|
||||
// inert — улучшение. Скрытая/переключаемая область блокируется и в старых движках.
|
||||
const savedTabIndexes = new Map();
|
||||
function lockVisual(value) {
|
||||
visual.style.visibility = value ? 'hidden' : 'visible';
|
||||
visual.style.pointerEvents = value ? 'none' : '';
|
||||
if (value) visual.setAttribute('inert', '');
|
||||
else visual.removeAttribute('inert');
|
||||
if (value) {
|
||||
visual.setAttribute('aria-hidden', 'true');
|
||||
for (const node of visual.querySelectorAll('a,button,input,select,textarea,[tabindex]')) {
|
||||
if (!savedTabIndexes.has(node)) savedTabIndexes.set(node, node.getAttribute('tabindex'));
|
||||
node.tabIndex = -1;
|
||||
}
|
||||
if (visual.contains(document.activeElement)) document.activeElement.blur();
|
||||
} else {
|
||||
visual.removeAttribute('aria-hidden');
|
||||
for (const [node, index] of savedTabIndexes) {
|
||||
if (index === null) node.removeAttribute('tabindex');
|
||||
else node.setAttribute('tabindex', index);
|
||||
}
|
||||
savedTabIndexes.clear();
|
||||
}
|
||||
}
|
||||
for (const name of ['click', 'mousedown', 'touchstart', 'pointerdown', 'keydown', 'keyup']) {
|
||||
visual.addEventListener(name, event => {
|
||||
if (locked) { event.preventDefault(); event.stopImmediatePropagation(); }
|
||||
}, true);
|
||||
}
|
||||
|
||||
function commitPair(number, pair) {
|
||||
// Обе декодированные DOM-картинки заменяются в одном JS-такте, без промежуточного paint.
|
||||
replaceImage(element.querySelector('#image-compare-source'), pair[0]);
|
||||
@@ -168,6 +226,8 @@ window.ZoomzillaExamples = {
|
||||
root.dataset.examplesState = 'loading';
|
||||
root.setAttribute('aria-busy', 'true');
|
||||
retry.hidden = true;
|
||||
retry.disabled = true;
|
||||
lockVisual(true);
|
||||
refreshItems();
|
||||
try {
|
||||
const item = items.find(item => itemNumber(item) === active);
|
||||
@@ -183,9 +243,7 @@ window.ZoomzillaExamples = {
|
||||
items.forEach((item, index) => replaceImage(item.querySelector('.image-examples-image img'), thumbs[index]));
|
||||
commitPair(active, pair);
|
||||
locked = false;
|
||||
visual.style.visibility = 'visible';
|
||||
visual.inert = false;
|
||||
visual.removeAttribute('aria-hidden');
|
||||
lockVisual(false);
|
||||
root.dataset.examplesState = 'ready';
|
||||
root.setAttribute('aria-busy', 'false');
|
||||
refreshItems();
|
||||
@@ -211,6 +269,7 @@ window.ZoomzillaExamples = {
|
||||
root.dataset.examplesState = 'error';
|
||||
root.setAttribute('aria-busy', 'false');
|
||||
retry.hidden = false;
|
||||
retry.disabled = false;
|
||||
}
|
||||
}
|
||||
retry.addEventListener('click', run);
|
||||
@@ -218,7 +277,8 @@ window.ZoomzillaExamples = {
|
||||
if (root.dataset.examplesState === 'error') run();
|
||||
else { attemptedAt.clear(); items.forEach(retryFailed); }
|
||||
});
|
||||
media.addEventListener('change', run);
|
||||
if (media.addEventListener) media.addEventListener('change', run);
|
||||
else media.addListener(run);
|
||||
refreshItems();
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, {once: true});
|
||||
else run();
|
||||
|
||||
Reference in new issue
Block a user