/* GETCOURSE — LESSON UI / v0.4.5 */
(function () {
'use strict';
const LESSON_PATH_RE = /\/(?:pl\/)?teach\/control\/lesson\/view/;
const CONFIG_SELECTOR = 'script.psych-lesson-config[type="application/json"]';
if (!LESSON_PATH_RE.test(location.pathname)) return;
const isAdmin = !!(
window.userInfo &&
(window.userInfo.isAdmin || window.userInfo.isManager || window.userInfo.isTeacher)
);
const editMode = new URLSearchParams(window.location.search).get('editMode') === '1';
document.documentElement.classList.add('psych-lesson-ui-loading');
const guard = document.createElement('style');
guard.id = 'psych-lesson-ui-fouc';
guard.textContent = `
html.psych-lesson-ui-loading .standard-page-content {
visibility: hidden !important;
opacity: 0 !important;
}
`;
document.head.appendChild(guard);
const qs = (sel, root = document) => root.querySelector(sel);
const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const text = (el) => (el?.textContent || '').replace(/\s+/g, ' ').trim();
let trainingType = '';
function isFreeWebinarsTraining() {
return trainingType === 'free-webinars';
}
function decorateAnswerStatuses(root = document) {
qsa(
'.self-answers .answer_wrapper',
root
).forEach(card => {
card.classList.add(
'psych-lesson-answer-card'
);
const label =
qs(
'.answer-status-label',
card
);
if (!label) return;
const value =
text(label);
const danger =
/(?:не\s*принят|не\s*зачт|отклон)/i
.test(value) ||
card.classList.contains(
'status-declined'
) ||
card.classList.contains(
'status-not_accepted'
) ||
card.classList.contains(
'status-not-accepted'
);
label.classList.toggle(
'psych-answer-status-danger',
danger
);
});
}
async function resolveTrainingType() {
const moduleLink = qs('.page-header h1 a[href]');
const href = moduleLink?.href || '';
if (!href) return '';
const cacheKey = `psych-training-type:${href}`;
try {
const cached = sessionStorage.getItem(cacheKey);
if (cached !== null) return cached;
} catch (_) {}
try {
const response = await fetch(href, {
credentials: 'same-origin'
});
if (!response.ok) return '';
const html = await response.text();
const doc = new DOMParser().parseFromString(
html,
'text/html'
);
const type = String(
doc.querySelector(
'.psych-training-type[data-training-type]'
)?.getAttribute(
'data-training-type'
) || ''
)
.trim()
.toLowerCase();
try {
sessionStorage.setItem(
cacheKey,
type
);
} catch (_) {}
return type;
} catch (err) {
console.warn(
'[lesson-ui] Training type loading failed',
err
);
return '';
}
}
function safeUrl(url) {
try {
return new URL(url, location.origin).href;
} catch (_) {
return '';
}
}
function readConfig() {
const node = qs(CONFIG_SELECTOR);
if (!node) {
return {
version: 1,
video: {
alternative: {
enabled: false,
type: 'playerjs',
url: '',
poster: ''
}
},
materials: [],
ui: {
materialsButtonLabel: 'Скачать материалы'
}
};
}
try {
const parsed = JSON.parse(node.textContent || '{}');
return {
version: parsed.version || 1,
video: {
alternative: {
enabled: !!parsed?.video?.alternative?.enabled,
type: parsed?.video?.alternative?.type || 'playerjs',
url: parsed?.video?.alternative?.url || '',
poster: parsed?.video?.alternative?.poster || ''
}
},
materials: Array.isArray(parsed.materials) ? parsed.materials : [],
ui: {
materialsButtonLabel:
parsed?.ui?.materialsButtonLabel || 'Скачать материалы'
}
};
} catch (err) {
console.warn('[lesson-ui] Invalid lesson config JSON', err);
return {
version: 1,
video: { alternative: { enabled: false, type: 'playerjs', url: '', poster: '' } },
materials: [],
ui: { materialsButtonLabel: 'Скачать материалы' }
};
}
}
let config = null;
function getConfig() {
if (!config) config = readConfig();
return config;
}
function ensureRuntimeConfigNode() {
let node = qs(CONFIG_SELECTOR);
if (node) return node;
node = document.createElement('script');
node.type = 'application/json';
node.className = 'psych-lesson-config';
node.dataset.psychRuntimeConfig = '1';
node.textContent = JSON.stringify(getConfig(), null, 2);
(document.body || document.documentElement).appendChild(node);
return node;
}
function hasPersistentConfigNode() {
const node = qs(CONFIG_SELECTOR);
return !!(node && node.dataset.psychRuntimeConfig !== '1');
}
function buildConfigHtml() {
const json = JSON.stringify(
normalizeConfigShape(JSON.parse(JSON.stringify(getConfig()))),
null,
2
);
return `<script type="application/json" class="psych-lesson-config">\n${json}\n</script>`;
}
function getLessonData() {
const lessonTitle = text(qs('.lesson-title-value')) || document.title;
const lessonDescription = text(qs('.lesson-description-value'));
const moduleLink = qs('.page-header h1 a[href]');
const moduleTitle = text(moduleLink);
const nav = qs('.lesson-navigation');
const progressText = text(nav?.querySelector('td:nth-child(2) span'));
const prev = nav?.querySelector('td:first-child a[href]') || null;
const next = nav?.querySelector('td:last-child a[href]') || null;
const statusText = text(nav?.querySelector('.user-state-label'));
return {
lessonTitle,
lessonDescription,
moduleTitle,
moduleHref: moduleLink?.href || '',
progressText,
prevHref: prev?.href || '',
nextHref: next?.href || '',
nextTitle: text(nav?.querySelector('td:last-child .hidden-xs')),
prevTitle: text(nav?.querySelector('td:first-child .hidden-xs')),
statusText
};
}
function findGcPlayerBlock() {
return (
qs('.o-lt-video .vhi-root.js--vhi-root')?.closest('.lite-block-live-wrapper') ||
qs('.o-lt-video .vhi-root')?.closest('.lite-block-live-wrapper') ||
qs('.o-lt-video iframe')?.closest('.lite-block-live-wrapper') ||
null
);
}
function decorateBase() {
document.body.classList.add('psych-lesson-ui');
if (isAdmin) document.body.classList.add('psych-lesson-ui--admin');
const standard = qs('.standard-page-content');
if (standard) standard.classList.add('psych-lesson-standard-page');
qs('.page-header')?.classList.add('psych-lesson-native-page-header');
qs('.lesson-header-block')?.classList.add('psych-lesson-native-header');
qs('.lite-page')?.classList.add('psych-lesson-content');
qs('.lt-lesson-mission-block')?.closest('.lite-block-live-wrapper')
?.classList.add('psych-lesson-mission-shell');
const commentsShell = qs('.lt-lesson-comment-block')?.closest('.lite-block-live-wrapper');
if (commentsShell) {
commentsShell.classList.add('psych-lesson-comments-shell');
if (!isAdmin) {
commentsShell.classList.add('psych-lesson-comments-shell--admin-only');
commentsShell.setAttribute('aria-hidden', 'true');
}
}
decorateAnswerStatuses();
qsa('.comments-tree-wrapper .gc-comment').forEach(el => {
el.classList.add('psych-lesson-comment-card');
});
qsa('.gc-comment-form').forEach(el => {
el.classList.add('psych-lesson-comment-form');
});
}
function normalizeLessonForms() {
qsa('.gc-comment-form').forEach(formRoot => {
formRoot.classList.add('psych-lesson-comment-form');
const textareaBlock = qs('.textarea-block', formRoot);
if (!textareaBlock) return;
const emojiEditor = qs('.emoji-wysiwyg-editor', textareaBlock);
const nativeTextarea = qs('textarea.new-comment-textarea', textareaBlock);
textareaBlock.classList.toggle('psych-has-emoji-editor', !!emojiEditor);
if (emojiEditor && nativeTextarea) {
nativeTextarea.classList.add('psych-emoji-source-textarea');
nativeTextarea.style.setProperty('display', 'none', 'important');
nativeTextarea.setAttribute('aria-hidden', 'true');
} else if (nativeTextarea) {
nativeTextarea.classList.remove('psych-emoji-source-textarea');
nativeTextarea.style.removeProperty('display');
nativeTextarea.removeAttribute('aria-hidden');
}
let actions = qs(':scope > .psych-lesson-comment-actions', textareaBlock);
if (!actions) {
actions = document.createElement('div');
actions.className = 'psych-lesson-comment-actions';
textareaBlock.appendChild(actions);
}
const attach = qs(':scope > .attach-file-button-container', textareaBlock);
const send = qs(':scope > .btn-send', textareaBlock);
if (attach && attach.parentElement !== actions) actions.appendChild(attach);
if (send && send.parentElement !== actions) actions.appendChild(send);
});
qsa('.answer-form .answer-textarea-container').forEach(container => {
const emojiEditor = qs('.emoji-wysiwyg-editor', container);
const nativeTextarea = qs('textarea.emoji-textarea', container);
container.classList.toggle('psych-has-emoji-editor', !!emojiEditor);
if (emojiEditor) {
emojiEditor.classList.add('psych-primary-answer-emoji-editor');
const editorWrap =
emojiEditor.closest('.emoji-wysiwyg-editor-parent') ||
emojiEditor.parentElement;
if (editorWrap && container.contains(editorWrap)) {
editorWrap.classList.add('psych-primary-answer-emoji-wrap');
const emojiButton =
qs('.emoji-button', container) ||
qs('.emoji-picker-icon', container);
if (emojiButton && emojiButton.parentElement !== editorWrap) {
editorWrap.appendChild(emojiButton);
}
}
}
if (emojiEditor && nativeTextarea) {
nativeTextarea.classList.add('psych-emoji-source-textarea');
nativeTextarea.style.setProperty('display', 'none', 'important');
nativeTextarea.setAttribute('aria-hidden', 'true');
} else if (nativeTextarea) {
nativeTextarea.classList.remove('psych-emoji-source-textarea');
nativeTextarea.style.removeProperty('display');
nativeTextarea.removeAttribute('aria-hidden');
}
});
}
function watchLessonForms() {
normalizeLessonForms();
const root =
qs('.lesson-mission-wrapper') ||
qs('.psych-lesson-content') ||
document.body;
let scheduled = false;
const observer = new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
normalizeLessonForms();
});
});
observer.observe(root, {
childList: true,
subtree: true
});
}
function decorateLessonBreadcrumbs() {
const native =
qs('.standard-page-content > .breadcrumb') ||
qs('.standard-page-content .breadcrumb');
if (!native || qs('.psych-lesson-breadcrumbs')) return;
native.classList.add('psych-lesson-native-breadcrumbs');
const custom = document.createElement('nav');
custom.className = 'psych-lesson-breadcrumbs';
custom.setAttribute('aria-label', 'Хлебные крошки');
const links = qsa('a[href]', native);
links.forEach((link, index) => {
const clone = link.cloneNode(true);
const label = text(clone);
if (
label === 'Список тренингов' ||
label === 'Тренинги'
) {
clone.textContent = 'Мои тренинги';
}
custom.appendChild(clone);
if (index < links.length - 1) {
const sep = document.createElement('span');
sep.className = 'psych-lesson-breadcrumbs__sep';
sep.textContent = '›';
sep.setAttribute('aria-hidden', 'true');
custom.appendChild(sep);
}
});
native.parentNode.insertBefore(custom, native);
}
function buildLessonHero() {
if (qs('.psych-lesson-hero')) return;
const nativeHeader = qs('.lesson-header-block');
if (!nativeHeader) return;
const data = getLessonData();
const hero = document.createElement('section');
hero.className = 'psych-lesson-hero';
hero.innerHTML = `
${data.moduleHref && data.moduleTitle ? `
<a class="psych-lesson-back-to-module"
href="${escapeAttr(data.moduleHref)}">
<span class="psych-lesson-back-to-module__arrow">←</span>
<span class="psych-lesson-back-to-module__text">
<small>Вернуться в раздел</small>
<strong>${escapeHtml(data.moduleTitle)}</strong>
</span>
</a>
` : ''}
<div class="psych-lesson-hero__top">
<div class="psych-lesson-hero__main">
<h1>${escapeHtml(data.lessonTitle)}</h1>
<div class="psych-lesson-hero__meta">
${data.lessonDescription ? `<span>${escapeHtml(data.lessonDescription)}</span>` : ''}
${data.progressText ? `<span>${escapeHtml(data.progressText)}</span>` : ''}
${data.statusText ? `<span class="psych-lesson-status-chip">${escapeHtml(data.statusText)}</span>` : ''}
</div>
</div>
<div class="psych-lesson-hero__nav">
${data.prevHref ? `<a class="psych-lesson-nav-btn psych-lesson-nav-btn--ghost" href="${data.prevHref}">← ${isFreeWebinarsTraining() ? 'Предыдущий вебинар' : 'Предыдущий урок'}</a>` : ''}
${data.nextHref ? `<a class="psych-lesson-nav-btn" href="${data.nextHref}">${isFreeWebinarsTraining() ? 'Следующий вебинар' : 'Следующий урок'} →</a>` : ''}
</div>
</div>
`;
nativeHeader.parentNode.insertBefore(hero, nativeHeader);
nativeHeader.style.display = 'none';
}
function buildPlayerShell() {
if (qs('.psych-lesson-player-shell')) return;
const block = findGcPlayerBlock();
if (!block) return;
const shell = document.createElement('section');
shell.className = 'psych-lesson-player-shell';
block.parentNode.insertBefore(shell, block);
shell.appendChild(block);
block.classList.add('psych-lesson-player-shell__gc');
renderPlayerShellFooter(shell, block);
}
function renderPlayerShellFooter(shell, block) {
qs('.psych-lesson-player-shell__footer', shell)?.remove();
const footer = document.createElement('div');
footer.className = 'psych-lesson-player-shell__footer';
const alt = getConfig().video.alternative;
const hasAlt = !!(alt.enabled && alt.url);
const help = document.createElement('div');
help.className = 'psych-lesson-player-help';
help.innerHTML = `
<span class="psych-lesson-player-help__dot">i</span>
<span>Проблемы с видео?</span>
`;
if (hasAlt) {
const switchBtn = document.createElement('button');
switchBtn.type = 'button';
switchBtn.className = 'psych-lesson-link-button';
switchBtn.textContent = 'Открыть альтернативный плеер';
switchBtn.addEventListener('click', () => {
showAlternativePlayer(shell, block, switchBtn);
});
help.appendChild(switchBtn);
}
footer.appendChild(help);
const materialsButton = buildMaterialsButton();
if (materialsButton) footer.appendChild(materialsButton);
if (!hasAlt && !materialsButton) {
footer.classList.add('psych-lesson-player-shell__footer--empty');
}
shell.appendChild(footer);
}
function refreshPlayerShell() {
const shell = qs('.psych-lesson-player-shell');
if (!shell) {
buildPlayerShell();
return;
}
const block = qs('.psych-lesson-player-shell__gc', shell) || findGcPlayerBlock();
if (!block) return;
qs('.psych-lesson-alt-player', shell)?.remove();
block.style.display = '';
renderPlayerShellFooter(shell, block);
}
function showAlternativePlayer(shell, gcBlock, sourceButton) {
let altWrap = qs('.psych-lesson-alt-player', shell);
if (!altWrap) {
altWrap = document.createElement('div');
altWrap.className = 'psych-lesson-alt-player';
const alt = getConfig().video.alternative;
const videoUrl = safeUrl(alt.url);
const posterUrl = safeUrl(alt.poster);
altWrap.innerHTML = `
<div class="psych-lesson-alt-player__stage">
<video
class="psych-lesson-alt-player__video"
playsinline
preload="metadata"
controlslist="nodownload noremoteplayback"
disablepictureinpicture
${posterUrl ? `poster="${escapeAttr(posterUrl)}"` : ''}
>
<source src="${escapeAttr(videoUrl)}">
</video>
<button type="button"
class="psych-lesson-alt-player__big-play"
aria-label="Воспроизвести">
<svg class="icon-play" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8.5 6.7v10.6c0 .8.9 1.3 1.6.9l8-5.3a1 1 0 0 0 0-1.7l-8-5.3c-.7-.5-1.6 0-1.6.8Z"></path>
</svg>
<svg class="icon-pause" viewBox="0 0 24 24" aria-hidden="true" style="display:none">
<rect x="7" y="6" width="3" height="12" rx="1"></rect>
<rect x="14" y="6" width="3" height="12" rx="1"></rect>
</svg>
</button>
<div class="psych-lesson-alt-controls">
<button type="button"
class="psych-lesson-alt-control psych-lesson-alt-play"
aria-label="Воспроизведение">
<svg class="icon-play" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8.5 6.7v10.6c0 .8.9 1.3 1.6.9l8-5.3a1 1 0 0 0 0-1.7l-8-5.3c-.7-.5-1.6 0-1.6.8Z"></path>
</svg>
<svg class="icon-pause" viewBox="0 0 24 24" aria-hidden="true" style="display:none">
<rect x="7" y="6" width="3" height="12" rx="1"></rect>
<rect x="14" y="6" width="3" height="12" rx="1"></rect>
</svg>
</button>
<button type="button"
class="psych-lesson-alt-control psych-lesson-alt-back"
aria-label="Назад на 10 секунд">
<span>−10</span>
</button>
<button type="button"
class="psych-lesson-alt-control psych-lesson-alt-forward"
aria-label="Вперёд на 10 секунд">
<span>+10</span>
</button>
<span class="psych-lesson-alt-time">
0:00 / 0:00
</span>
<input type="range"
class="psych-lesson-alt-progress"
min="0"
max="1000"
value="0"
step="1"
aria-label="Позиция видео">
<button type="button"
class="psych-lesson-alt-control psych-lesson-alt-mute"
aria-label="Звук">
<svg class="icon-volume" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 10v4h3l4 4V6L8 10H5Z"></path>
<path d="M15 9.5c1 .7 1.6 1.5 1.6 2.5s-.6 1.8-1.6 2.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
</svg>
<svg class="icon-muted" viewBox="0 0 24 24" aria-hidden="true" style="display:none">
<path d="M5 10v4h3l4 4V6L8 10H5Z"></path>
<path d="m16 10 4 4m0-4-4 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
</svg>
</button>
<input type="range"
class="psych-lesson-alt-volume"
min="0"
max="1"
step="0.01"
value="1"
aria-label="Громкость">
<select class="psych-lesson-alt-speed"
aria-label="Скорость воспроизведения">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="1.75">1.75×</option>
<option value="2">2×</option>
</select>
<button type="button"
class="psych-lesson-alt-control psych-lesson-alt-fullscreen"
aria-label="На весь экран">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 4H4v3m13-3h3v3M7 20H4v-3m13 3h3v-3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</div>
</div>
<div class="psych-lesson-alt-player__bar">
<div class="psych-lesson-alt-player__bar-left">
<span>Альтернативный источник</span>
<button type="button"
class="psych-lesson-link-button psych-lesson-alt-player__back-to-gc">
Вернуться к плееру GetCourse
</button>
</div>
</div>
`;
shell.insertBefore(altWrap, shell.firstChild);
setupAlternativePlayer(altWrap);
qs('.psych-lesson-alt-player__back-to-gc', altWrap)
?.addEventListener('click', () => {
const video = qs('.psych-lesson-alt-player__video', altWrap);
if (video) video.pause();
altWrap.hidden = true;
gcBlock.hidden = false;
shell.classList.remove('psych-lesson-player-shell--alt-open');
sourceButton.textContent = 'Открыть альтернативный плеер';
});
}
gcBlock.hidden = true;
altWrap.hidden = false;
shell.classList.add('psych-lesson-player-shell--alt-open');
sourceButton.textContent = 'Альтернативный плеер открыт';
}
function setupAlternativePlayer(root) {
const video = qs('.psych-lesson-alt-player__video', root);
const play = qs('.psych-lesson-alt-play', root);
const bigPlay = qs('.psych-lesson-alt-player__big-play', root);
const back = qs('.psych-lesson-alt-back', root);
const forward = qs('.psych-lesson-alt-forward', root);
const mute = qs('.psych-lesson-alt-mute', root);
const volume = qs('.psych-lesson-alt-volume', root);
const progress = qs('.psych-lesson-alt-progress', root);
const time = qs('.psych-lesson-alt-time', root);
const speed = qs('.psych-lesson-alt-speed', root);
const fullscreen = qs('.psych-lesson-alt-fullscreen', root);
const controls = qs('.psych-lesson-alt-controls', root);
const stage = qs('.psych-lesson-alt-player__stage', root);
if (!video || !stage) return;
let controlsTimer = 0;
let bigButtonTimer = 0;
let desiredRate = Number(speed?.value) || 1;
video.addEventListener('contextmenu', event => {
event.preventDefault();
});
const fmt = (seconds) => {
if (!Number.isFinite(seconds)) return '0:00';
const value = Math.max(0, Math.floor(seconds));
const minutes = Math.floor(value / 60);
const secs = String(value % 60).padStart(2, '0');
return `${minutes}:${secs}`;
};
const setIconState = (button, paused) => {
if (!button) return;
const playIcon = qs('.icon-play', button);
const pauseIcon = qs('.icon-pause', button);
if (playIcon) playIcon.style.display = paused ? '' : 'none';
if (pauseIcon) pauseIcon.style.display = paused ? 'none' : '';
};
const showControls = () => {
stage.classList.remove('psych-lesson-alt-stage--controls-hidden');
if (controls) controls.setAttribute('aria-hidden', 'false');
clearTimeout(controlsTimer);
if (!video.paused) {
controlsTimer = window.setTimeout(() => {
stage.classList.add('psych-lesson-alt-stage--controls-hidden');
if (controls) controls.setAttribute('aria-hidden', 'true');
}, 2400);
}
};
const showBigButton = (temporary = false) => {
if (!bigPlay) return;
clearTimeout(bigButtonTimer);
bigPlay.classList.remove('is-hidden');
if (temporary && !video.paused) {
bigButtonTimer = window.setTimeout(() => {
bigPlay.classList.add('is-hidden');
}, 650);
}
};
const syncPlaybackRate = () => {
const rate = Number(desiredRate) || 1;
if (Math.abs(video.playbackRate - rate) > 0.01) {
try {
video.playbackRate = rate;
} catch (_) {}
}
try {
video.defaultPlaybackRate = rate;
} catch (_) {}
if (speed && String(speed.value) !== String(rate)) {
speed.value = String(rate);
}
};
const update = () => {
const duration = Number.isFinite(video.duration) ? video.duration : 0;
const current = Number.isFinite(video.currentTime) ? video.currentTime : 0;
const paused = video.paused;
if (progress && duration > 0 && document.activeElement !== progress) {
progress.value = String(Math.round((current / duration) * 1000));
}
if (time) {
time.textContent = `${fmt(current)} / ${fmt(duration)}`;
}
setIconState(play, paused);
setIconState(bigPlay, paused);
if (bigPlay) {
bigPlay.setAttribute(
'aria-label',
paused ? 'Воспроизвести' : 'Пауза'
);
if (paused) {
clearTimeout(bigButtonTimer);
bigPlay.classList.remove('is-hidden');
}
}
};
const togglePlay = () => {
showControls();
if (video.paused) {
video.play()
.then(() => {
syncPlaybackRate();
update();
showBigButton(true);
})
.catch(() => {});
} else {
video.pause();
update();
showBigButton(false);
}
};
play?.addEventListener('click', event => {
event.stopPropagation();
togglePlay();
});
bigPlay?.addEventListener('click', event => {
event.stopPropagation();
togglePlay();
});
video.addEventListener('click', togglePlay);
stage.addEventListener('mousemove', showControls);
stage.addEventListener('mouseenter', showControls);
stage.addEventListener('touchstart', showControls, { passive: true });
stage.addEventListener('focusin', showControls);
controls?.addEventListener('click', event => {
event.stopPropagation();
showControls();
});
video.addEventListener('play', () => {
syncPlaybackRate();
update();
showControls();
showBigButton(true);
});
video.addEventListener('pause', () => {
update();
showControls();
showBigButton(false);
});
video.addEventListener('ended', () => {
update();
showControls();
showBigButton(false);
});
video.addEventListener('timeupdate', update);
video.addEventListener('loadedmetadata', () => {
syncPlaybackRate();
update();
});
video.addEventListener('durationchange', update);
back?.addEventListener('click', event => {
event.stopPropagation();
video.currentTime = Math.max(0, video.currentTime - 10);
showControls();
});
forward?.addEventListener('click', event => {
event.stopPropagation();
const max = Number.isFinite(video.duration)
? video.duration
: video.currentTime + 10;
video.currentTime = Math.min(max, video.currentTime + 10);
showControls();
});
progress?.addEventListener('input', event => {
event.stopPropagation();
if (!Number.isFinite(video.duration) || video.duration <= 0) return;
video.currentTime =
(Number(progress.value) / 1000) * video.duration;
showControls();
});
function syncVolumeUi() {
if (!mute) return;
const volumeIcon = qs('.icon-volume', mute);
const mutedIcon = qs('.icon-muted', mute);
const isMuted = video.muted || video.volume === 0;
if (volumeIcon) volumeIcon.style.display = isMuted ? 'none' : '';
if (mutedIcon) mutedIcon.style.display = isMuted ? '' : 'none';
if (volume && document.activeElement !== volume) {
volume.value = String(video.muted ? 0 : video.volume);
}
}
mute?.addEventListener('click', event => {
event.stopPropagation();
if (video.muted || video.volume === 0) {
video.muted = false;
if (video.volume === 0) video.volume = 1;
} else {
video.muted = true;
}
syncVolumeUi();
showControls();
});
volume?.addEventListener('input', event => {
event.stopPropagation();
const nextVolume = Math.min(
1,
Math.max(0, Number(volume.value))
);
video.volume = nextVolume;
video.muted = nextVolume === 0;
syncVolumeUi();
showControls();
});
video.addEventListener('volumechange', syncVolumeUi);
const setSpeed = event => {
event?.stopPropagation?.();
desiredRate = Number(speed?.value) || 1;
syncPlaybackRate();
showControls();
};
speed?.addEventListener('input', setSpeed);
speed?.addEventListener('change', setSpeed);
video.addEventListener('ratechange', () => {
if (Math.abs(video.playbackRate - desiredRate) > 0.01) {
syncPlaybackRate();
}
});
fullscreen?.addEventListener('click', async event => {
event.stopPropagation();
showControls();
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
} else if (stage.requestFullscreen) {
await stage.requestFullscreen();
} else if (video.webkitEnterFullscreen) {
video.webkitEnterFullscreen();
}
} catch (_) {}
});
document.addEventListener('fullscreenchange', () => {
syncPlaybackRate();
showControls();
});
video.addEventListener('webkitbeginfullscreen', () => {
syncPlaybackRate();
});
video.addEventListener('webkitendfullscreen', () => {
syncPlaybackRate();
showControls();
});
syncVolumeUi();
syncPlaybackRate();
update();
showControls();
}
function getFileExtension(item) {
const explicit = (item.ext || item.extension || '').toLowerCase().replace('.', '');
if (explicit) return explicit;
try {
const pathname = new URL(item.url, location.origin).pathname;
const name = pathname.split('/').pop() || '';
const match = name.match(/\.([a-z0-9]{1,8})$/i);
return (match?.[1] || '').toLowerCase();
} catch (_) {
return '';
}
}
function getFileIcon(ext) {
const map = {
pdf: 'PDF',
doc: 'DOC', docx: 'DOC',
xls: 'XLS', xlsx: 'XLS', csv: 'CSV',
ppt: 'PPT', pptx: 'PPT',
zip: 'ZIP', rar: 'ZIP', '7z': 'ZIP',
jpg: 'IMG', jpeg: 'IMG', png: 'IMG', webp: 'IMG', gif: 'IMG', svg: 'IMG',
mp3: 'AUD', wav: 'AUD', ogg: 'AUD', m4a: 'AUD',
mp4: 'VID', mov: 'VID', webm: 'VID', avi: 'VID',
txt: 'TXT', rtf: 'TXT', md: 'TXT'
};
return map[ext] || 'FILE';
}
function buildMaterialsButton() {
const materials = (getConfig().materials || []).filter(
item => item && item.url && item.visible !== false
);
if (!materials.length) return null;
const wrap = document.createElement('div');
wrap.className = 'psych-lesson-materials-control';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'psych-lesson-materials-button';
btn.innerHTML = `
<span class="psych-lesson-materials-button__icon">↓</span>
<span>${escapeHtml(getConfig().ui.materialsButtonLabel || 'Скачать материалы')}</span>
`;
const popover = document.createElement('div');
popover.className = 'psych-lesson-materials-popover';
popover.hidden = true;
const sorted = [...materials].sort(
(a, b) => (a.order ?? 999) - (b.order ?? 999)
);
popover.innerHTML = `
<div class="psych-lesson-materials-popover__head">
<strong>Материалы урока</strong>
<button type="button" class="psych-lesson-materials-popover__close">×</button>
</div>
<div class="psych-lesson-materials-list">
${sorted.map(item => {
const ext = getFileExtension(item);
return `
<a class="psych-lesson-material-item"
href="${escapeAttr(safeUrl(item.url))}"
target="_blank"
rel="noopener">
<span class="psych-lesson-file-icon psych-lesson-file-icon--${escapeAttr(ext || 'file')}">
${escapeHtml(getFileIcon(ext))}
</span>
<span class="psych-lesson-material-item__body">
<strong>${escapeHtml(item.title || item.name || 'Материал')}</strong>
<span>
${ext ? escapeHtml(ext.toUpperCase()) : 'Файл'}
${item.size ? ` · ${escapeHtml(item.size)}` : ''}
</span>
</span>
<span class="psych-lesson-material-item__arrow">&#8599;</span>
</a>
`;
}).join('')}
</div>
`;
btn.addEventListener('click', () => {
if (materials.length === 1) {
window.open(safeUrl(materials[0].url), '_blank', 'noopener');
return;
}
popover.hidden = !popover.hidden;
});
qs('.psych-lesson-materials-popover__close', popover)
?.addEventListener('click', () => {
popover.hidden = true;
});
wrap.append(btn, popover);
return wrap;
}
function getViewModeUrl() {
const url = new URL(window.location.href);
url.searchParams.set('editMode', '0');
return url.href;
}
function buildEditModeReturn() {
if (!isAdmin || !editMode || qs('.psych-lesson-edit-return')) return;
const target =
qs('.psych-lesson-admin-toolbar') ||
qs('.psych-lesson-native-page-header') ||
qs('.standard-page-content');
if (!target) return;
const bar = document.createElement('div');
bar.className = 'psych-lesson-edit-return';
bar.innerHTML = `
<a class="psych-lesson-edit-return__button"
href="${escapeAttr(getViewModeUrl())}">
← Вернуться к просмотру урока
</a>
<span>Режим редактирования</span>
`;
if (target.classList.contains('psych-lesson-admin-toolbar')) {
target.insertAdjacentElement('beforebegin', bar);
} else {
target.insertAdjacentElement('afterbegin', bar);
}
}
function collectAdminActions() {
const pageHeader = qs('.page-header');
if (!pageHeader) return [];
const actions = [];
const seen = new Set();
qsa('a[href], button[onclick]', pageHeader).forEach(el => {
let href = '';
let label = text(el);
if (el.tagName === 'A') {
href = el.href;
} else {
const onclick = el.getAttribute('onclick') || '';
const match = onclick.match(/location\.href\s*=\s*['"]([^'"]+)['"]/);
if (match) href = safeUrl(match[1]);
}
if (!href || !label) return;
const key = `${label}|${href}`;
if (seen.has(key)) return;
seen.add(key);
actions.push({ label, href });
});
return actions;
}
function findAction(actions, matcher) {
const item = actions.find(a => matcher.test(a.label));
return item || null;
}
function detectMission() {
return !!qs('.lt-lesson-mission-block');
}
function detectComments() {
return !!qs('.lt-lesson-comment-block');
}
function countVisibleAnswerWrappers() {
return qsa('.lt-lesson-comment-block .answer_wrapper').length;
}
function buildAdminToolbar() {
if (!isAdmin || qs('.psych-lesson-admin-toolbar')) return;
const hero = qs('.psych-lesson-hero');
if (!hero) return;
const actions = collectAdminActions();
const edit = findAction(actions, /Редактировать урок/i);
const bar = document.createElement('div');
bar.className = 'psych-lesson-admin-toolbar';
bar.innerHTML = `
<div class="psych-lesson-admin-toolbar__label">Режим администратора</div>
<div class="psych-lesson-admin-toolbar__actions">
${edit ? `<a class="psych-lesson-admin-btn psych-lesson-admin-btn--ghost" href="${escapeAttr(edit.href)}">Редактировать</a>` : ''}
<button type="button" class="psych-lesson-admin-btn psych-lesson-admin-open">
Управление уроком
</button>
</div>
`;
hero.insertAdjacentElement('beforebegin', bar);
qs('.psych-lesson-admin-open', bar)
?.addEventListener('click', openAdminPanel);
}
function buildAdminPanel() {
if (!isAdmin || qs('.psych-lesson-admin-panel')) return;
const data = getLessonData();
const actions = collectAdminActions();
const panel = document.createElement('aside');
panel.className = 'psych-lesson-admin-panel';
panel.hidden = true;
panel.innerHTML = `
<div class="psych-lesson-admin-panel__backdrop"></div>
<div class="psych-lesson-admin-panel__sheet">
<div class="psych-lesson-admin-panel__head">
<div>
<span>Управление уроком</span>
<strong>${escapeHtml(data.lessonTitle)}</strong>
</div>
<button type="button" class="psych-lesson-admin-close" aria-label="Закрыть">×</button>
</div>
<div class="psych-lesson-admin-tabs" role="tablist">
${[
['general', 'Общее'],
['video', 'Видео'],
['materials', 'Материалы'],
['mission', 'Задание'],
['answers', 'Ответы'],
['actions', 'Действия']
].map(([id, label], i) => `
<button type="button"
class="psych-lesson-admin-tab ${i === 0 ? 'is-active' : ''}"
data-tab="${id}">
${label}
</button>
`).join('')}
</div>
<div class="psych-lesson-admin-status" hidden></div>
<div class="psych-lesson-admin-panels">
<section class="psych-lesson-admin-section is-active" data-panel="general">
${renderGeneralPanel(data)}
</section>
<section class="psych-lesson-admin-section" data-panel="video">
${renderVideoPanel()}
</section>
<section class="psych-lesson-admin-section" data-panel="materials">
${renderMaterialsPanel()}
</section>
<section class="psych-lesson-admin-section" data-panel="mission">
${renderMissionPanel(actions)}
</section>
<section class="psych-lesson-admin-section" data-panel="answers">
${renderAnswersPanel(actions)}
</section>
<section class="psych-lesson-admin-section" data-panel="actions">
${renderActionsPanel(actions)}
</section>
</div>
</div>
`;
document.body.appendChild(panel);
qsa('.psych-lesson-admin-tab', panel).forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.tab;
qsa('.psych-lesson-admin-tab', panel)
.forEach(x => x.classList.toggle('is-active', x === btn));
qsa('.psych-lesson-admin-section', panel)
.forEach(section => {
section.classList.toggle(
'is-active',
section.dataset.panel === id
);
});
});
});
qs('.psych-lesson-admin-close', panel)?.addEventListener('click', closeAdminPanel);
qs('.psych-lesson-admin-panel__backdrop', panel)?.addEventListener('click', closeAdminPanel);
bindAdminEditors(panel);
}
function renderGeneralPanel(data) {
const hasPlayer = !!findGcPlayerBlock();
const visibleMaterials = (getConfig().materials || []).filter(
x => x && x.url && x.visible !== false
);
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-summary">
${summaryRow('Раздел', data.moduleTitle || '—')}
${summaryRow('Урок', data.lessonTitle || '—')}
${summaryRow('Позиция', data.progressText || '—')}
${summaryRow('Видео', hasPlayer ? 'Есть' : 'Не найдено', hasPlayer)}
${summaryRow(
'Резервное видео',
getConfig().video.alternative.enabled && getConfig().video.alternative.url ? 'Подключено' : 'Нет',
!!(getConfig().video.alternative.enabled && getConfig().video.alternative.url)
)}
${summaryRow('Материалы', `${visibleMaterials.length} шт.`)}
${summaryRow('Задание', detectMission() ? 'Есть' : 'Нет', detectMission())}
</div>
</div>
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-card__title-row">
<div>
<div class="psych-lesson-admin-card__title">Конфигурация интерфейса</div>
<div class="psych-lesson-admin-card__subtitle">
${hasPersistentConfigNode()
? 'В уроке найден постоянный блок <code>psych-lesson-config</code>. Изменения можно проверить сразу на странице.'
: 'В этом уроке ещё нет постоянного блока конфигурации. Панель создала временную конфигурацию, поэтому настройка уже доступна.'
}
</div>
</div>
<span class="psych-lesson-config-state ${hasPersistentConfigNode() ? 'is-saved' : 'is-runtime'}">
${hasPersistentConfigNode() ? 'Блок найден' : 'Временная'}
</span>
</div>
<div class="psych-lesson-admin-config-note">
${hasPersistentConfigNode()
? 'После изменения скопируйте готовый HTML-блок и замените им текущий блок конфигурации в уроке.'
: 'После настройки нажмите «Скопировать HTML-блок» и один раз вставьте его в HTML-блок урока. До этого изменения сохраняются только до перезагрузки страницы.'
}
</div>
<div class="psych-lesson-admin-form-actions">
${hasPersistentConfigNode() ? `
<button type="button" class="psych-lesson-admin-btn psych-lesson-admin-save-config">
Сохранить настройки
</button>
` : ''}
<button type="button" class="psych-lesson-admin-btn psych-lesson-admin-btn--ghost psych-lesson-admin-copy-config">
Скопировать HTML-блок
</button>
</div>
</div>
`;
}
function renderVideoPanel() {
const hasPlayer = !!findGcPlayerBlock();
const alt = getConfig().video.alternative;
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-card__title">Основной плеер</div>
<div class="psych-lesson-admin-diagnostic">
<span class="psych-lesson-admin-dot ${hasPlayer ? 'is-ok' : 'is-muted'}"></span>
<div>
<strong>${hasPlayer ? 'Плеер GetCourse найден' : 'Плеер не найден'}</strong>
<span>Штатный плеер остаётся источником прогресса и просмотра.</span>
</div>
</div>
</div>
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-card__title">Альтернативный плеер</div>
<div class="psych-lesson-admin-card__subtitle">
Используется как резервный источник, если у ученика возникают проблемы с основным видео.
</div>
<div class="psych-lesson-admin-form">
<label class="psych-lesson-admin-switch">
<input type="checkbox"
data-admin-alt-enabled
${alt.enabled ? 'checked' : ''}>
<span></span>
<strong>Показывать альтернативный плеер</strong>
</label>
<label class="psych-lesson-admin-field">
<span>Тип плеера</span>
<select data-admin-alt-type>
<option value="html5" ${alt.type === 'html5' || alt.type === 'playerjs' ? 'selected' : ''}>HTML5 video</option>
</select>
</label>
<label class="psych-lesson-admin-field">
<span>Ссылка на видео</span>
<input type="url"
data-admin-alt-url
value="${escapeAttr(alt.url || '')}"
placeholder="https://.../video.mp4">
<small>Лучше использовать прямую ссылку на MP4/WebM из файлового хранилища.</small>
</label>
<label class="psych-lesson-admin-field">
<span>Постер</span>
<input type="url"
data-admin-alt-poster
value="${escapeAttr(alt.poster || '')}"
placeholder="https://.../poster.jpg">
</label>
<div class="psych-lesson-admin-form-actions">
<button type="button"
class="psych-lesson-admin-btn psych-lesson-admin-alt-apply">
Применить
</button>
<button type="button"
class="psych-lesson-admin-btn psych-lesson-admin-btn--ghost psych-lesson-admin-alt-open"
${alt.url ? '' : 'disabled'}>
Открыть видео
</button>
</div>
</div>
</div>
`;
}
function renderMaterialsPanel() {
const materials = Array.isArray(getConfig().materials)
? getConfig().materials
: [];
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-card__title-row">
<div>
<div class="psych-lesson-admin-card__title">Материалы урока</div>
<div class="psych-lesson-admin-card__subtitle">
Добавляйте прямые ссылки на файлы из хранилища GetCourse. Иконка определяется по расширению файла автоматически.
</div>
</div>
<button type="button"
class="psych-lesson-admin-btn psych-lesson-admin-btn--ghost psych-lesson-admin-material-add">
+ Добавить
</button>
</div>
<div class="psych-lesson-admin-material-editor">
${materials.length
? materials.map((item, index) => renderMaterialEditorRow(item, index)).join('')
: `
<div class="psych-lesson-admin-empty psych-lesson-admin-material-empty">
Материалов пока нет. Нажмите «Добавить».
</div>
`
}
</div>
<div class="psych-lesson-admin-form-actions">
<button type="button"
class="psych-lesson-admin-btn psych-lesson-admin-materials-apply">
Применить материалы
</button>
</div>
</div>
`;
}
function renderMaterialEditorRow(item = {}, index = 0) {
return `
<div class="psych-lesson-admin-material-row" data-material-row>
<div class="psych-lesson-admin-material-row__head">
<strong>Материал ${index + 1}</strong>
<button type="button"
class="psych-lesson-admin-material-remove"
aria-label="Удалить материал">×</button>
</div>
<div class="psych-lesson-admin-form-grid">
<label class="psych-lesson-admin-field psych-lesson-admin-field--wide">
<span>Название</span>
<input type="text"
data-material-title
value="${escapeAttr(item.title || item.name || '')}"
placeholder="Рабочая тетрадь">
</label>
<label class="psych-lesson-admin-field psych-lesson-admin-field--wide">
<span>Ссылка на файл</span>
<input type="url"
data-material-url
value="${escapeAttr(item.url || '')}"
placeholder="https://.../file.pdf">
</label>
<label class="psych-lesson-admin-field">
<span>Размер</span>
<input type="text"
data-material-size
value="${escapeAttr(item.size || '')}"
placeholder="2,4 МБ">
</label>
<label class="psych-lesson-admin-field">
<span>Порядок</span>
<input type="number"
data-material-order
value="${escapeAttr(item.order ?? index + 1)}"
min="1"
step="1">
</label>
</div>
<label class="psych-lesson-admin-switch psych-lesson-admin-switch--compact">
<input type="checkbox"
data-material-visible
${item.visible === false ? '' : 'checked'}>
<span></span>
<strong>Показывать ученику</strong>
</label>
</div>
`;
}
function normalizeConfigShape(value) {
const next = value && typeof value === 'object' ? value : {};
if (!next.version) next.version = 1;
if (!next.video) next.video = {};
if (!next.video.alternative) next.video.alternative = {};
if (!Array.isArray(next.materials)) next.materials = [];
if (!next.ui) next.ui = {};
if (!next.ui.materialsButtonLabel) {
next.ui.materialsButtonLabel = 'Скачать материалы';
}
next.video.alternative = {
enabled: !!next.video.alternative.enabled,
type: 'html5',
url: String(next.video.alternative.url || '').trim(),
poster: String(next.video.alternative.poster || '').trim()
};
return next;
}
function applyConfig(nextConfig, message = 'Изменения применены.') {
config = normalizeConfigShape(nextConfig);
const node = qs(CONFIG_SELECTOR) || (isAdmin ? ensureRuntimeConfigNode() : null);
if (node) {
node.textContent = JSON.stringify(config, null, 2);
}
refreshPlayerShell();
refreshAdminDynamicPanels();
setAdminStatus(message, 'success');
}
function refreshAdminDynamicPanels() {
const panel = qs('.psych-lesson-admin-panel');
if (!panel) return;
const general = qs('[data-panel="general"]', panel);
const video = qs('[data-panel="video"]', panel);
const materials = qs('[data-panel="materials"]', panel);
if (general) general.innerHTML = renderGeneralPanel(getLessonData());
if (video) video.innerHTML = renderVideoPanel();
if (materials) materials.innerHTML = renderMaterialsPanel();
bindAdminEditors(panel);
}
function bindAdminEditors(panel) {
if (!panel) return;
qs('.psych-lesson-admin-alt-apply', panel)?.addEventListener('click', () => {
const next = JSON.parse(JSON.stringify(getConfig()));
next.video = next.video || {};
next.video.alternative = {
enabled: !!qs('[data-admin-alt-enabled]', panel)?.checked,
type: 'html5',
url: String(qs('[data-admin-alt-url]', panel)?.value || '').trim(),
poster: String(qs('[data-admin-alt-poster]', panel)?.value || '').trim()
};
applyConfig(next, 'Настройки альтернативного плеера применены.');
});
qs('.psych-lesson-admin-alt-open', panel)?.addEventListener('click', () => {
const url = String(qs('[data-admin-alt-url]', panel)?.value || '').trim();
const resolved = safeUrl(url);
if (resolved) window.open(resolved, '_blank', 'noopener');
});
qs('[data-admin-alt-url]', panel)?.addEventListener('input', (event) => {
const button = qs('.psych-lesson-admin-alt-open', panel);
if (button) button.disabled = !String(event.currentTarget.value || '').trim();
});
qs('.psych-lesson-admin-material-add', panel)?.addEventListener('click', () => {
const editor = qs('.psych-lesson-admin-material-editor', panel);
if (!editor) return;
qs('.psych-lesson-admin-material-empty', editor)?.remove();
const temp = document.createElement('div');
temp.innerHTML = renderMaterialEditorRow({}, qsa('[data-material-row]', editor).length);
const row = temp.firstElementChild;
if (row) {
editor.appendChild(row);
bindMaterialRow(row);
}
});
qsa('[data-material-row]', panel).forEach(bindMaterialRow);
qs('.psych-lesson-admin-materials-apply', panel)?.addEventListener('click', () => {
const rows = qsa('[data-material-row]', panel);
const next = JSON.parse(JSON.stringify(getConfig()));
next.materials = rows
.map((row, index) => ({
title: String(qs('[data-material-title]', row)?.value || '').trim(),
url: String(qs('[data-material-url]', row)?.value || '').trim(),
size: String(qs('[data-material-size]', row)?.value || '').trim(),
order: Number(qs('[data-material-order]', row)?.value || index + 1),
visible: !!qs('[data-material-visible]', row)?.checked
}))
.filter(item => item.title || item.url)
.map((item, index) => ({
...item,
order: Number.isFinite(item.order) && item.order > 0 ? item.order : index + 1
}));
applyConfig(next, 'Материалы урока применены.');
});
qsa('.psych-lesson-admin-copy-config', panel).forEach(button => {
button.addEventListener('click', copyCurrentConfig);
});
qsa('.psych-lesson-admin-save-config', panel).forEach(button => {
button.addEventListener('click', () => saveConfigToGetCourse(button));
});
}
function bindMaterialRow(row) {
const remove = qs('.psych-lesson-admin-material-remove', row);
if (!remove || remove.dataset.bound === '1') return;
remove.dataset.bound = '1';
remove.addEventListener('click', () => {
const editor = row.parentElement;
row.remove();
if (editor && !qs('[data-material-row]', editor)) {
editor.innerHTML = `
<div class="psych-lesson-admin-empty psych-lesson-admin-material-empty">
Материалов пока нет. Нажмите «Добавить».
</div>
`;
}
if (editor) {
qsa('[data-material-row]', editor).forEach((item, index) => {
const title = qs('.psych-lesson-admin-material-row__head strong', item);
if (title) title.textContent = `Материал ${index + 1}`;
});
}
});
}
function getPersistentConfigBlockMeta() {
const node = qs(CONFIG_SELECTOR);
if (!node || node.dataset.psychRuntimeConfig === '1') return null;
const wrapper =
node.closest('.lite-block-live-wrapper[data-block-id]') ||
node.closest('.lt-block[data-block-id]');
const block =
node.closest('.lt-block[data-block-id]') ||
wrapper?.querySelector('.lt-block[data-block-id]');
const blockId =
wrapper?.dataset.blockId ||
block?.dataset.blockId ||
'';
const dataCode =
block?.dataset.code ||
wrapper?.querySelector('.lt-block[data-code]')?.dataset.code ||
'';
const blockCode = String(dataCode || '').replace(/^b-/, '');
if (!blockId) return null;
return {
blockId: String(blockId),
blockCode
};
}
function parseSettingsFormHtml(html) {
const doc = new DOMParser().parseFromString(
`<form id="psych-temp-settings-form">${html || ''}</form>`,
'text/html'
);
const form = doc.querySelector('#psych-temp-settings-form');
const payload = {};
if (!form) return payload;
form.querySelectorAll('[name]').forEach(control => {
const name = control.getAttribute('name');
if (!name) return;
const tag = control.tagName.toLowerCase();
const type = String(control.getAttribute('type') || '').toLowerCase();
if ((type === 'checkbox' || type === 'radio') && !control.checked) {
return;
}
if (tag === 'select' && control.multiple) {
payload[name] = Array.from(control.selectedOptions).map(o => o.value);
return;
}
payload[name] = control.value ?? '';
});
return payload;
}
function gcAjax(url, data = {}) {
return new Promise((resolve, reject) => {
if (typeof window.ajaxCall === 'function') {
try {
window.ajaxCall(
url,
data,
{},
response => resolve(response),
error => reject(error)
);
return;
} catch (err) {
reject(err);
return;
}
}
if (window.jQuery?.ajax) {
window.jQuery.ajax({
url,
type: 'POST',
data,
dataType: 'json',
success: resolve,
error: (_, __, error) => reject(error || new Error('AJAX error'))
});
return;
}
reject(new Error('GetCourse AJAX helper unavailable'));
});
}
async function saveConfigToGetCourse(button) {
const meta = getPersistentConfigBlockMeta();
if (!meta) {
setAdminStatus(
'Постоянный HTML-блок psych-lesson-config не найден.',
'error'
);
return;
}
const originalText = button?.textContent || 'Сохранить настройки';
if (button) {
button.disabled = true;
button.textContent = 'Сохраняю…';
}
setAdminStatus('Сохраняю настройки в HTML-блок GetCourse…', 'info');
try {
const settingsResponse = await gcAjax(
`/pl/lite/block/settings?id=${encodeURIComponent(meta.blockId)}`,
{ place: 'content' }
);
const settingsHtml =
settingsResponse?.data?.html ||
settingsResponse?.html ||
'';
const payload = parseSettingsFormHtml(settingsHtml);
payload['blockParam[rawValue]'] = buildConfigHtml();
if (!payload['blockParam[blockCode]'] && meta.blockCode) {
payload['blockParam[blockCode]'] = meta.blockCode;
}
if (!('blockParam[replaceVariables]' in payload)) {
payload['blockParam[replaceVariables]'] = '1';
}
const saveResponse = await gcAjax(
`/pl/lite/block/save-settings?id=${encodeURIComponent(meta.blockId)}`,
payload
);
if (saveResponse && saveResponse.success === false) {
throw new Error(
saveResponse.message ||
saveResponse.error ||
'GetCourse отклонил сохранение'
);
}
const node = qs(CONFIG_SELECTOR);
if (node) {
node.textContent = JSON.stringify(
normalizeConfigShape(JSON.parse(JSON.stringify(getConfig()))),
null,
2
);
}
setAdminStatus('Настройки сохранены в HTML-блоке GetCourse.', 'success');
} catch (err) {
console.error('[lesson-ui] Save config failed', err);
setAdminStatus(
`Не удалось сохранить настройки${err?.message ? `: ${err.message}` : '.'}`,
'error'
);
} finally {
if (button) {
button.disabled = false;
button.textContent = originalText;
}
}
}
async function copyCurrentConfig() {
const value = buildConfigHtml();
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
} else {
const area = document.createElement('textarea');
area.value = value;
area.style.position = 'fixed';
area.style.opacity = '0';
document.body.appendChild(area);
area.select();
document.execCommand('copy');
area.remove();
}
setAdminStatus(
'Готовый HTML-блок скопирован. Вставьте его в HTML-блок урока.',
'success'
);
} catch (err) {
console.error('[lesson-ui] Copy config failed', err);
setAdminStatus('Не удалось скопировать HTML-блок конфигурации.', 'error');
}
}
function setAdminStatus(message, type = 'info') {
const status = qs('.psych-lesson-admin-status');
if (!status) return;
status.textContent = message;
status.className = `psych-lesson-admin-status is-${type}`;
status.hidden = false;
clearTimeout(setAdminStatus.timer);
setAdminStatus.timer = setTimeout(() => {
status.hidden = true;
}, 4200);
}
function renderMissionPanel(actions) {
const mission = findAction(actions, /Задание/i);
const settings = findAction(actions, /Настройки/i);
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-summary">
${summaryRow('Задание', detectMission() ? 'Есть' : 'Не найдено', detectMission())}
</div>
<div class="psych-lesson-admin-quick">
${mission ? actionLink(mission.label, mission.href) : ''}
${settings ? actionLink(settings.label, settings.href) : ''}
</div>
</div>
`;
}
function renderAnswersPanel(actions) {
const check = findAction(actions, /Проверка заданий/i);
const count = countVisibleAnswerWrappers();
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-summary">
${summaryRow('Блок ответов', detectComments() ? 'Есть' : 'Не найден', detectComments())}
${summaryRow('Ответов в текущем DOM', String(count))}
</div>
<div class="psych-lesson-admin-quick">
${check ? actionLink(check.label, check.href) : ''}
</div>
</div>
`;
}
function renderActionsPanel(actions) {
if (!actions.length) {
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-empty">Штатные действия GetCourse не найдены.</div>
</div>
`;
}
const ordered = [
/Редактировать урок/i,
/Настройки/i,
/Задание/i,
/Проверка заданий/i,
/уведомление/i,
/Копировать урок/i,
/Перенести урок/i,
/История изменений/i,
/Удалить урок/i
];
const result = [];
const used = new Set();
ordered.forEach(re => {
const item = actions.find((a, i) => re.test(a.label) && !used.has(i));
if (item) {
const idx = actions.indexOf(item);
used.add(idx);
result.push(item);
}
});
actions.forEach((item, idx) => {
if (!used.has(idx)) result.push(item);
});
return `
<div class="psych-lesson-admin-card">
<div class="psych-lesson-admin-action-list">
${result.map(item => {
const danger = /Удалить/i.test(item.label);
return `
<a class="psych-lesson-admin-action ${danger ? 'is-danger' : ''}"
href="${escapeAttr(item.href)}">
<span>${escapeHtml(item.label)}</span>
<span>→</span>
</a>
`;
}).join('')}
</div>
</div>
`;
}
function summaryRow(label, value, positive = null) {
return `
<div class="psych-lesson-admin-summary__row">
<span>${escapeHtml(label)}</span>
<strong class="${positive === true ? 'is-positive' : positive === false ? 'is-muted' : ''}">
${escapeHtml(value)}
</strong>
</div>
`;
}
function actionLink(label, href) {
return `
<a class="psych-lesson-admin-action" href="${escapeAttr(href)}">
<span>${escapeHtml(label)}</span>
<span>→</span>
</a>
`;
}
function openAdminPanel() {
const panel = qs('.psych-lesson-admin-panel');
if (!panel) return;
panel.hidden = false;
document.body.classList.add('psych-lesson-admin-panel-open');
}
function closeAdminPanel() {
const panel = qs('.psych-lesson-admin-panel');
if (!panel) return;
panel.hidden = true;
document.body.classList.remove('psych-lesson-admin-panel-open');
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '\u0026amp;')
.replace(/</g, '\u0026lt;')
.replace(/>/g, '\u0026gt;')
.replace(/"/g, '\u0026quot;')
.replace(/'/g, '\u0026#039;');
}
function escapeAttr(value) {
return escapeHtml(value);
}
function finish() {
document.documentElement.classList.add('psych-lesson-ui-ready');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.documentElement.classList.remove('psych-lesson-ui-loading');
qs('#psych-lesson-ui-fouc')?.remove();
});
});
}
async function init() {
try {
config = readConfig();
if (isAdmin && !qs(CONFIG_SELECTOR)) {
ensureRuntimeConfigNode();
}
decorateBase();
decorateLessonBreadcrumbs();
watchLessonForms();
trainingType =
await resolveTrainingType();
if (isFreeWebinarsTraining()) {
document.documentElement.classList.add(
'psych-lesson-free-webinars'
);
document.body.classList.add(
'psych-lesson-free-webinars'
);
}
buildLessonHero();
buildPlayerShell();
buildAdminToolbar();
buildEditModeReturn();
buildAdminPanel();
setTimeout(() => {
decorateAnswerStatuses();
qsa('.comments-tree-wrapper .gc-comment').forEach(el => {
el.classList.add('psych-lesson-comment-card');
});
qsa('.gc-comment-form').forEach(el => {
el.classList.add('psych-lesson-comment-form');
});
normalizeLessonForms();
}, 350);
} catch (err) {
console.error('[lesson-ui]', err);
} finally {
finish();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();