fix(sidebar): убрать мигание ссылок «Подготовка к экзамену» при отключении
Ссылки exam-prep (/exam-prep/*) скрывались отдельным async-механизмом (/api/exam-prep/ tracks), не входящим в синхронный кэш-CSS → мелькали на долю секунды после обновления. Теперь hideDisabledFeatures кэширует точные хрефы скрытых треков в localStorage (ls_examhide), а _applyFeatureCss добавляет их в инъект-CSS синхронно из кэша на ранней загрузке (до сборки сайдбара). При включении трека он убирается из кэша → снова виден (re-apply _applyFeatureCss после свежего fetch). hideEmptySidebarGroups перенесён в конец hideDisabledFeatures (учитывает скрытие exam-prep). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -862,14 +862,21 @@ const FEATURE_WIDGETS = {
|
|||||||
на ранней загрузке (ДО построения сайдбара/виджетов) — против мигания (FOUC),
|
на ранней загрузке (ДО построения сайдбара/виджетов) — против мигания (FOUC),
|
||||||
затем обновляется по свежему /api/features. */
|
затем обновляется по свежему /api/features. */
|
||||||
function _applyFeatureCss(feats) {
|
function _applyFeatureCss(feats) {
|
||||||
if (!feats) return;
|
|
||||||
const sels = [];
|
const sels = [];
|
||||||
for (const [key, hrefs] of Object.entries(FEATURE_HREFS)) {
|
if (feats) {
|
||||||
if (feats[key] === false) {
|
for (const [key, hrefs] of Object.entries(FEATURE_HREFS)) {
|
||||||
hrefs.forEach(h => sels.push(`[href="${h}"]`));
|
if (feats[key] === false) {
|
||||||
(FEATURE_WIDGETS[key] || []).forEach(s => sels.push(s));
|
hrefs.forEach(h => sels.push(`[href="${h}"]`));
|
||||||
|
(FEATURE_WIDGETS[key] || []).forEach(s => sels.push(s));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Скрытые exam-prep треки (подготовка): кэш хрефов с прошлой загрузки — против мигания.
|
||||||
|
// /api/exam-prep/tracks асинхронен, поэтому держим точный список скрытых ссылок в кэше.
|
||||||
|
try {
|
||||||
|
JSON.parse(localStorage.getItem('ls_examhide') || '[]')
|
||||||
|
.forEach(h => sels.push(`[href="${h}"]`));
|
||||||
|
} catch { /* пусто */ }
|
||||||
const css = sels.length ? sels.join(',') + '{display:none !important}' : '';
|
const css = sels.length ? sels.join(',') + '{display:none !important}' : '';
|
||||||
let el = document.getElementById('ls-feat-hide');
|
let el = document.getElementById('ls-feat-hide');
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -879,13 +886,13 @@ function _applyFeatureCss(feats) {
|
|||||||
}
|
}
|
||||||
el.textContent = css;
|
el.textContent = css;
|
||||||
// Геймификация: класс на <html> (доступен раньше body) → kill-switch без мигания.
|
// Геймификация: класс на <html> (доступен раньше body) → kill-switch без мигания.
|
||||||
document.documentElement.classList.toggle('no-gamification', feats.gamification === false);
|
if (feats) document.documentElement.classList.toggle('no-gamification', feats.gamification === false);
|
||||||
}
|
}
|
||||||
/* Ранняя синхронная попытка из кэша прошлой загрузки — нет мигания на повторных заходах.
|
/* Ранняя синхронная попытка из кэша прошлой загрузки — нет мигания на повторных заходах.
|
||||||
(FEATURE_HREFS — const, поэтому этот вызов идёт ПОСЛЕ его объявления.) */
|
(FEATURE_HREFS — const, поэтому этот вызов идёт ПОСЛЕ его объявления.) */
|
||||||
try {
|
try {
|
||||||
const _cachedFeats = JSON.parse(localStorage.getItem('ls_feat_cache') || 'null');
|
const _cachedFeats = JSON.parse(localStorage.getItem('ls_feat_cache') || 'null');
|
||||||
if (_cachedFeats) _applyFeatureCss(_cachedFeats);
|
_applyFeatureCss(_cachedFeats); // применит и кэш фич, и кэш скрытых exam-prep ссылок
|
||||||
} catch { /* нет кэша / приватный режим — просто ждём async */ }
|
} catch { /* нет кэша / приватный режим — просто ждём async */ }
|
||||||
|
|
||||||
/* Прячет группы сайдбара (.sb-group), у которых не осталось ни одного видимого пункта,
|
/* Прячет группы сайдбара (.sb-group), у которых не осталось ни одного видимого пункта,
|
||||||
@@ -942,7 +949,6 @@ async function hideDisabledFeatures() {
|
|||||||
document.querySelector('[onclick*="tab-account"]')?.click();
|
document.querySelector('[onclick*="tab-account"]')?.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hideEmptySidebarGroups(); // после обновления видимости пунктов — спрятать пустые группы
|
|
||||||
|
|
||||||
// Exam-prep track links (/exam-prep/<key>): показываем только включённые
|
// Exam-prep track links (/exam-prep/<key>): показываем только включённые
|
||||||
// (exam_tracks.enabled) и доступные пользователю треки. /api/exam-prep/tracks
|
// (exam_tracks.enabled) и доступные пользователю треки. /api/exam-prep/tracks
|
||||||
@@ -952,10 +958,16 @@ async function hideDisabledFeatures() {
|
|||||||
try {
|
try {
|
||||||
const data = await apiFetch('/api/exam-prep/tracks');
|
const data = await apiFetch('/api/exam-prep/tracks');
|
||||||
const allowed = new Set((data.tracks || []).map(t => t.exam_key));
|
const allowed = new Set((data.tracks || []).map(t => t.exam_key));
|
||||||
|
// Собираем точные хрефы скрытых треков и кэшируем — чтобы на СЛЕДУЮЩЕЙ загрузке
|
||||||
|
// _applyFeatureCss спрятал их синхронно из кэша ещё до сборки сайдбара (без мигания).
|
||||||
|
const hide = [];
|
||||||
examLinks.forEach(el => {
|
examLinks.forEach(el => {
|
||||||
const m = (el.getAttribute('href') || '').match(/^\/exam-prep\/([^/?#]+)/);
|
const href = el.getAttribute('href') || '';
|
||||||
if (m && !allowed.has(m[1])) el.style.display = 'none';
|
const m = href.match(/^\/exam-prep\/([^/?#]+)/);
|
||||||
|
if (m && !allowed.has(m[1])) hide.push(href);
|
||||||
});
|
});
|
||||||
|
try { localStorage.setItem('ls_examhide', JSON.stringify(hide)); } catch {}
|
||||||
|
_applyFeatureCss(_featuresCache); // обновить <style> (скрыть запрещённые, ПОКАЗАТЬ снова разрешённые)
|
||||||
const cur = window.location.pathname.match(/^\/exam-prep\/([^/?#]+)/);
|
const cur = window.location.pathname.match(/^\/exam-prep\/([^/?#]+)/);
|
||||||
if (cur && !allowed.has(cur[1])) window.location.href = '/dashboard.html';
|
if (cur && !allowed.has(cur[1])) window.location.href = '/dashboard.html';
|
||||||
} catch { /* сеть/доступ недоступны — ссылки оставляем как есть */ }
|
} catch { /* сеть/доступ недоступны — ссылки оставляем как есть */ }
|
||||||
@@ -986,6 +998,9 @@ async function hideDisabledFeatures() {
|
|||||||
document.body.classList.add('no-class');
|
document.body.classList.add('no-class');
|
||||||
document.body.classList.add('no-gamification'); // no class <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> no gamification
|
document.body.classList.add('no-gamification'); // no class <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> no gamification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// В самом конце — после всех скрытий (фичи, exam-prep, no_class) — схлопнуть пустые группы.
|
||||||
|
hideEmptySidebarGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── generic authenticated fetch (full path like /api/courses) ─────── */
|
/* ── generic authenticated fetch (full path like /api/courses) ─────── */
|
||||||
|
|||||||
Reference in New Issue
Block a user