1226 lines
45 KiB
JavaScript
1226 lines
45 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
function initLottieIcons() {
|
|
function loadLottieScript() {
|
|
if (typeof window.lottie !== 'undefined') return Promise.resolve(true);
|
|
|
|
return new Promise(function (resolve) {
|
|
const existing = document.querySelector('script[data-lottie-loader="true"]');
|
|
if (existing) {
|
|
existing.addEventListener('load', function () { resolve(typeof window.lottie !== 'undefined'); });
|
|
existing.addEventListener('error', function () { resolve(false); });
|
|
return;
|
|
}
|
|
|
|
const script = document.createElement('script');
|
|
script.src = '/js/lib/lottie.min.js';
|
|
script.async = true;
|
|
script.dataset.lottieLoader = 'true';
|
|
script.addEventListener('load', function () { resolve(typeof window.lottie !== 'undefined'); });
|
|
script.addEventListener('error', function () { resolve(false); });
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
const icons = document.querySelectorAll('.lottie-icon[data-lottie-url]');
|
|
|
|
function initOne(el) {
|
|
const url = el.getAttribute('data-lottie-url');
|
|
if (!url || el.dataset.lottieInited === 'true') return;
|
|
|
|
const fallbackImg = el.parentElement ? el.parentElement.querySelector('.lottie-fallback') : null;
|
|
el.dataset.lottieInited = 'true';
|
|
|
|
try {
|
|
const anim = window.lottie.loadAnimation({
|
|
container: el,
|
|
renderer: 'svg',
|
|
loop: !reduceMotion,
|
|
autoplay: !reduceMotion,
|
|
path: url,
|
|
rendererSettings: { progressiveLoad: true },
|
|
});
|
|
|
|
if (fallbackImg) fallbackImg.style.display = 'none';
|
|
|
|
anim.addEventListener('data_failed', function () {
|
|
el.dataset.lottieInited = 'false';
|
|
if (fallbackImg) fallbackImg.style.display = '';
|
|
});
|
|
} catch (_error) {
|
|
el.dataset.lottieInited = 'false';
|
|
if (fallbackImg) fallbackImg.style.display = '';
|
|
}
|
|
}
|
|
|
|
loadLottieScript().then(function (ok) {
|
|
if (!ok || typeof window.lottie === 'undefined') return;
|
|
icons.forEach(initOne);
|
|
});
|
|
}
|
|
|
|
function initRankToggle() {
|
|
const button = document.querySelector('.btn-rank-toggle');
|
|
const popup = document.getElementById('rankListPopup');
|
|
|
|
if (!button || !popup) return;
|
|
|
|
button.addEventListener('click', function () {
|
|
const isOpen = button.classList.toggle('is-open');
|
|
popup.hidden = !isOpen;
|
|
button.setAttribute('aria-expanded', String(isOpen));
|
|
});
|
|
|
|
document.addEventListener('click', function (event) {
|
|
if (!button.classList.contains('is-open')) return;
|
|
if (button.contains(event.target) || popup.contains(event.target)) return;
|
|
button.classList.remove('is-open');
|
|
popup.hidden = true;
|
|
button.setAttribute('aria-expanded', 'false');
|
|
});
|
|
}
|
|
|
|
function initButtons() {
|
|
document.querySelectorAll('.btn-reset-goal, .btn-next-growth').forEach(function (button) {
|
|
const nextUrl = button.getAttribute('data-next-url') || button.getAttribute('data-reset-url');
|
|
if (!nextUrl) return;
|
|
|
|
button.addEventListener('click', function () {
|
|
window.location.href = nextUrl;
|
|
});
|
|
});
|
|
}
|
|
|
|
function ensureSectionRevealVisible() {
|
|
document.querySelectorAll('.scroll-section-reveal').forEach(function (section) {
|
|
section.classList.add('is-visible');
|
|
});
|
|
}
|
|
|
|
function initVideoModal() {
|
|
const COMMENT_MAX_LENGTH = 200;
|
|
|
|
const VideoModalCtor =
|
|
(typeof window !== 'undefined' && typeof window.VideoModalManager === 'function')
|
|
? window.VideoModalManager
|
|
: (typeof VideoModalManager === 'function' ? VideoModalManager : null);
|
|
|
|
if (typeof VideoModalCtor !== 'function' || !Array.isArray(window.MYCLASS_PAGE_VIDEOS)) {
|
|
console.warn('[myclass_list] initVideoModal failed:', {
|
|
VideoModalCtor: typeof VideoModalCtor,
|
|
MYCLASS_PAGE_VIDEOS: window.MYCLASS_PAGE_VIDEOS?.length
|
|
});
|
|
return;
|
|
}
|
|
|
|
console.log('[myclass_list] MYCLASS_PAGE_VIDEOS loaded:', window.MYCLASS_PAGE_VIDEOS);
|
|
|
|
// 같은 학습 목표 영상 필터링을 위해 전체 영상 백업
|
|
var allVideosBackup = (window.MYCLASS_PAGE_VIDEOS || []).slice();
|
|
|
|
const modalManager = new VideoModalCtor({ videos: window.MYCLASS_PAGE_VIDEOS });
|
|
if (typeof modalManager.init === 'function') {
|
|
modalManager.init();
|
|
}
|
|
// --- 영상 모달 관련 이벤트 초기화 ---
|
|
function clearUnsavedCommentDraft(modalRoot) {
|
|
if (!modalRoot) return;
|
|
|
|
const fields = modalRoot.querySelectorAll([
|
|
'.comment-box textarea',
|
|
'.comment-box input[type="text"]',
|
|
'.comment-write textarea',
|
|
'.comment-write input[type="text"]',
|
|
'textarea[name*="comment"]',
|
|
'input[type="text"][name*="comment"]',
|
|
'.comment-box [contenteditable="true"]',
|
|
'.comment-write [contenteditable="true"]'
|
|
].join(','));
|
|
|
|
fields.forEach(function (field) {
|
|
if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return;
|
|
if (field.disabled || field.readOnly) return;
|
|
if (!field.value) return;
|
|
|
|
field.value = '';
|
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
|
|
modalRoot.querySelectorAll('.comment-box [contenteditable="true"], .comment-write [contenteditable="true"]').forEach(function (editable) {
|
|
if (!(editable instanceof HTMLElement)) return;
|
|
if (editable.getAttribute('contenteditable') !== 'true') return;
|
|
if (!editable.textContent) return;
|
|
editable.textContent = '';
|
|
editable.dispatchEvent(new Event('input', { bubbles: true }));
|
|
editable.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
function scheduleCommentDraftReset(modalRoot) {
|
|
const delays = [0, 30, 80, 160, 300, 500];
|
|
delays.forEach(function (delay) {
|
|
setTimeout(function () {
|
|
clearUnsavedCommentDraft(modalRoot || getActiveVideoModalRoot());
|
|
}, delay);
|
|
});
|
|
}
|
|
|
|
function clearRenderedCommentArea(modalRoot) {
|
|
if (!modalRoot) return;
|
|
|
|
modalRoot.querySelectorAll([
|
|
'.comment-list',
|
|
'.comment-history',
|
|
'.comment-items',
|
|
'.comment-box .comment-list',
|
|
'.comment-box .history-list',
|
|
'.comment-box ul.comment-list'
|
|
].join(',')).forEach(function (node) {
|
|
if (node instanceof HTMLElement) {
|
|
node.innerHTML = '';
|
|
}
|
|
});
|
|
|
|
modalRoot.querySelectorAll([
|
|
'.comment-box textarea[disabled]',
|
|
'.comment-box .user-text',
|
|
'.comment-list textarea',
|
|
'.comment-history textarea'
|
|
].join(',')).forEach(function (node) {
|
|
if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) {
|
|
node.value = '';
|
|
} else if (node instanceof HTMLElement) {
|
|
node.textContent = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshRenderedComments() {
|
|
const modalRoot = getActiveVideoModalRoot();
|
|
if (!modalRoot) return;
|
|
|
|
clearRenderedCommentArea(modalRoot);
|
|
|
|
if (typeof modalManager.setupCommentBox === 'function') {
|
|
try { modalManager.setupCommentBox(); } catch (_e) {}
|
|
}
|
|
if (typeof modalManager.showCommentSection === 'function') {
|
|
try { modalManager.showCommentSection(); } catch (_e) {}
|
|
}
|
|
if (typeof modalManager.adjustCommentOnlyLayout === 'function') {
|
|
try { modalManager.adjustCommentOnlyLayout(); } catch (_e) {}
|
|
}
|
|
}
|
|
|
|
function scheduleCommentRefresh() {
|
|
[40, 120, 260].forEach(function (delay) {
|
|
setTimeout(function () {
|
|
refreshRenderedComments();
|
|
}, delay);
|
|
});
|
|
}
|
|
|
|
function getCommentDraftFields(modalRoot) {
|
|
if (!modalRoot) return [];
|
|
const selectors = [
|
|
'.comment-box textarea',
|
|
'.comment-box input[type="text"]',
|
|
'.comment-write textarea',
|
|
'.comment-write input[type="text"]',
|
|
'textarea[name*="comment"]',
|
|
'input[type="text"][name*="comment"]'
|
|
].join(',');
|
|
|
|
return Array.from(modalRoot.querySelectorAll(selectors)).filter(function (field) {
|
|
if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return false;
|
|
if (field.classList.contains('user-text') || field.closest('.comment-list')) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function clampCommentLength(field) {
|
|
if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return;
|
|
if (typeof field.value !== 'string') return;
|
|
if (field.value.length <= COMMENT_MAX_LENGTH) return;
|
|
|
|
field.value = field.value.slice(0, COMMENT_MAX_LENGTH);
|
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
function applyCommentInputLimit(modalRoot) {
|
|
getCommentDraftFields(modalRoot || getActiveVideoModalRoot()).forEach(function (field) {
|
|
field.maxLength = COMMENT_MAX_LENGTH;
|
|
clampCommentLength(field);
|
|
});
|
|
}
|
|
|
|
function scheduleCommentInputLimitApply(modalRoot) {
|
|
[0, 40, 120, 260, 500].forEach(function (delay) {
|
|
setTimeout(function () {
|
|
applyCommentInputLimit(modalRoot || getActiveVideoModalRoot());
|
|
}, delay);
|
|
});
|
|
}
|
|
|
|
function isCurrentVideoCompletedForComment() {
|
|
const video = modalManager && modalManager.currentVideo ? modalManager.currentVideo : null;
|
|
if (!video) return false;
|
|
|
|
const contentId = String(video.content_id || video.id || '').trim();
|
|
const watchTm = Math.max(0, Number.parseInt(video.watch_tm ?? 0, 10) || 0);
|
|
const contentTm = Math.max(
|
|
0,
|
|
Number.parseInt(video.content_tm ?? video.duration ?? 0, 10) || 0
|
|
);
|
|
|
|
const completedByRatio = contentTm > 0 && watchTm >= contentTm * 0.9;
|
|
const completedByFlag = !!(video.completed_at || video.completed === true);
|
|
|
|
let completedByCardClass = false;
|
|
if (contentId) {
|
|
const card = document.querySelector('.books-item[data-video-id="' + contentId + '"]');
|
|
completedByCardClass = !!(card && card.classList.contains('books-item-completed'));
|
|
}
|
|
|
|
return completedByRatio || completedByFlag || completedByCardClass;
|
|
}
|
|
|
|
function applyCommentWriteGate() {
|
|
const modalRoot = getActiveVideoModalRoot();
|
|
if (!modalRoot) return;
|
|
|
|
const canWrite = isCurrentVideoCompletedForComment();
|
|
|
|
const inputSelectors = [
|
|
'.comment-box textarea',
|
|
'.comment-box input[type="text"]',
|
|
'.comment-write textarea',
|
|
'.comment-write input[type="text"]',
|
|
'textarea[name*="comment"]',
|
|
'input[type="text"][name*="comment"]'
|
|
].join(',');
|
|
|
|
modalRoot.querySelectorAll(inputSelectors).forEach(function (field) {
|
|
if (!(field instanceof HTMLTextAreaElement) && !(field instanceof HTMLInputElement)) return;
|
|
// 읽기용 댓글 항목은 제외
|
|
if (field.classList.contains('user-text') || field.closest('.comment-list')) return;
|
|
field.maxLength = COMMENT_MAX_LENGTH;
|
|
field.disabled = false;
|
|
field.readOnly = !canWrite;
|
|
clampCommentLength(field);
|
|
if (!canWrite) {
|
|
field.setAttribute('placeholder', '시청 완료 후 작성 가능합니다.');
|
|
} else {
|
|
field.removeAttribute('placeholder');
|
|
}
|
|
});
|
|
|
|
modalRoot.querySelectorAll('.comment-box .btn-save, .comment-write .btn-save, .comment-box .btn-comment, .comment-box .btn-write').forEach(function (btn) {
|
|
if (!(btn instanceof HTMLButtonElement)) return;
|
|
btn.disabled = !canWrite;
|
|
btn.classList.toggle('is-disabled', !canWrite);
|
|
});
|
|
}
|
|
|
|
function scheduleCommentGateApply() {
|
|
[0, 40, 120, 260, 500].forEach(function (delay) {
|
|
setTimeout(function () {
|
|
applyCommentWriteGate();
|
|
}, delay);
|
|
});
|
|
}
|
|
|
|
function getActiveVideoModalRoot() {
|
|
if (modalManager && modalManager.currentModalElement instanceof HTMLElement) {
|
|
return modalManager.currentModalElement;
|
|
}
|
|
if (modalManager && modalManager.currentModal instanceof HTMLElement) {
|
|
return modalManager.currentModal;
|
|
}
|
|
return document.querySelector('.modal.video.is-open, .modal.video, .video-modal.is-open, .video-modal');
|
|
}
|
|
|
|
if (typeof modalManager.openVideo === 'function' && modalManager.__draftResetWrapped !== true) {
|
|
const originalOpenVideo = modalManager.openVideo.bind(modalManager);
|
|
modalManager.openVideo = function (...args) {
|
|
scheduleCommentDraftReset(getActiveVideoModalRoot());
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
|
|
|
|
const result = originalOpenVideo(...args);
|
|
if (result && typeof result.finally === 'function') {
|
|
return result.finally(function () {
|
|
// 모달 재사용 시 비동기 렌더 구간 전체에서 초기화
|
|
scheduleCommentDraftReset(getActiveVideoModalRoot());
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
|
|
});
|
|
}
|
|
|
|
scheduleCommentDraftReset(getActiveVideoModalRoot());
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
|
|
return result;
|
|
};
|
|
modalManager.__draftResetWrapped = true;
|
|
}
|
|
|
|
if (typeof modalManager.loadVideoModal === 'function' && modalManager.__draftResetLoadWrapped !== true) {
|
|
const originalLoadVideoModal = modalManager.loadVideoModal.bind(modalManager);
|
|
modalManager.loadVideoModal = function (...args) {
|
|
scheduleCommentDraftReset(getActiveVideoModalRoot());
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
|
|
|
|
const result = originalLoadVideoModal(...args);
|
|
if (result && typeof result.finally === 'function') {
|
|
return result.finally(function () {
|
|
scheduleCommentDraftReset(getActiveVideoModalRoot());
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
|
|
});
|
|
}
|
|
|
|
scheduleCommentDraftReset(getActiveVideoModalRoot());
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(getActiveVideoModalRoot());
|
|
return result;
|
|
};
|
|
modalManager.__draftResetLoadWrapped = true;
|
|
}
|
|
|
|
if (modalManager.__recommendedDraftResetBound !== true) {
|
|
const recommendedSelectors = [
|
|
'#recommendedList .list',
|
|
'.recommended-list .list',
|
|
'.video-list .list',
|
|
'.video-recommend-list .list',
|
|
'.modal.video .list[data-video-id]',
|
|
].join(',');
|
|
|
|
document.addEventListener('click', function (event) {
|
|
const target = event.target;
|
|
if (!target || !target.closest) return;
|
|
|
|
const modalRoot = target.closest('.modal.video, .video-modal, .modal.is-open');
|
|
if (!modalRoot) return;
|
|
|
|
// 페이지 카드(.books-item) 클릭과 구분하여 모달 내부 추천강의 클릭만 처리
|
|
if (target.closest('.books-item')) return;
|
|
|
|
const inRecommendedArea = !!target.closest(recommendedSelectors);
|
|
const hasVideoIdTarget = !!target.closest('[data-video-id]');
|
|
if (!inRecommendedArea && !hasVideoIdTarget) return;
|
|
|
|
scheduleCommentDraftReset(modalRoot);
|
|
scheduleCommentRefresh();
|
|
scheduleCommentGateApply();
|
|
scheduleCommentInputLimitApply(modalRoot);
|
|
}, true);
|
|
|
|
modalManager.__recommendedDraftResetBound = true;
|
|
}
|
|
|
|
if (modalManager.__commentGateSaveBlockBound !== true) {
|
|
document.addEventListener('click', function (event) {
|
|
const target = event.target;
|
|
if (!target || !target.closest) return;
|
|
const saveBtn = target.closest('.comment-box .btn-save, .comment-write .btn-save, .comment-box .btn-comment, .comment-box .btn-write');
|
|
if (!saveBtn) return;
|
|
|
|
const modalRoot = saveBtn.closest('.modal.video, .video-modal, .modal.is-open');
|
|
if (!modalRoot) return;
|
|
|
|
const overLimitField = getCommentDraftFields(modalRoot).find(function (field) {
|
|
return (field.value || '').length > COMMENT_MAX_LENGTH;
|
|
});
|
|
if (overLimitField) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
clampCommentLength(overLimitField);
|
|
alert('한줄 소감문은 200자까지 입력할 수 있습니다.');
|
|
return;
|
|
}
|
|
|
|
if (!isCurrentVideoCompletedForComment()) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
alert('시청 완료 후 한줄 소감문 작성이 가능합니다.');
|
|
}
|
|
}, true);
|
|
|
|
modalManager.__commentGateSaveBlockBound = true;
|
|
}
|
|
|
|
if (modalManager.__commentGateInputBlockBound !== true) {
|
|
let lastGateAlertAt = 0;
|
|
function showGateAlertThrottled() {
|
|
const now = Date.now();
|
|
if (now - lastGateAlertAt < 800) return;
|
|
lastGateAlertAt = now;
|
|
alert('시청 완료 후 한줄 소감문 작성이 가능합니다.');
|
|
}
|
|
|
|
const inputGateSelector = [
|
|
'.comment-box textarea',
|
|
'.comment-box input[type="text"]',
|
|
'.comment-write textarea',
|
|
'.comment-write input[type="text"]',
|
|
'textarea[name*="comment"]',
|
|
'input[type="text"][name*="comment"]'
|
|
].join(',');
|
|
|
|
document.addEventListener('focusin', function (event) {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement) || !target.closest) return;
|
|
const field = target.closest(inputGateSelector);
|
|
if (!field) return;
|
|
|
|
const modalRoot = field.closest('.modal.video, .video-modal, .modal.is-open');
|
|
if (!modalRoot) return;
|
|
if (field.classList.contains('user-text') || field.closest('.comment-list')) return;
|
|
if (isCurrentVideoCompletedForComment()) return;
|
|
|
|
showGateAlertThrottled();
|
|
if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) {
|
|
field.blur();
|
|
}
|
|
}, true);
|
|
|
|
document.addEventListener('click', function (event) {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement) || !target.closest) return;
|
|
const field = target.closest(inputGateSelector);
|
|
if (!field) return;
|
|
|
|
const modalRoot = field.closest('.modal.video, .video-modal, .modal.is-open');
|
|
if (!modalRoot) return;
|
|
if (field.classList.contains('user-text') || field.closest('.comment-list')) return;
|
|
if (isCurrentVideoCompletedForComment()) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
showGateAlertThrottled();
|
|
}, true);
|
|
|
|
document.addEventListener('input', function (event) {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLTextAreaElement) && !(target instanceof HTMLInputElement)) return;
|
|
if (!target.closest || !target.closest('.modal.video, .video-modal, .modal.is-open')) return;
|
|
if (target.classList.contains('user-text') || target.closest('.comment-list')) return;
|
|
|
|
if (target.value.length > COMMENT_MAX_LENGTH) {
|
|
target.value = target.value.slice(0, COMMENT_MAX_LENGTH);
|
|
alert('한줄 소감문은 200자까지 입력할 수 있습니다.');
|
|
}
|
|
}, true);
|
|
|
|
modalManager.__commentGateInputBlockBound = true;
|
|
}
|
|
|
|
function updateMyClassCommentIcon(contentId, hasComment) {
|
|
const id = String(contentId || '').trim();
|
|
if (!id) return;
|
|
|
|
const item = document.querySelector('.books-item[data-video-id="' + id + '"]');
|
|
if (!item) return;
|
|
|
|
const pencil = item.querySelector('.book-ico-pencil');
|
|
if (!pencil) return;
|
|
|
|
pencil.classList.toggle('book-ico-pencil-completed', !!hasComment);
|
|
pencil.setAttribute('title', hasComment ? '감상문 작성 완료' : '감상문 쓰기');
|
|
}
|
|
|
|
window.addEventListener('myclass:comment-status-changed', function (event) {
|
|
const detail = event && event.detail ? event.detail : {};
|
|
updateMyClassCommentIcon(detail.contentId, !!detail.hasComment);
|
|
});
|
|
|
|
// --- 영상모달 닫힐 때 포스트잇 즉시 표시 (새로고침 없이) ---
|
|
if (typeof modalManager === 'object' && typeof modalManager.close === 'function') {
|
|
const origClose = modalManager.close.bind(modalManager);
|
|
modalManager.close = async function(...args) {
|
|
// 닫기 전, 시청 완료 여부 확인
|
|
const video = this.currentVideo;
|
|
const contentId = video && (video.content_id || video.id);
|
|
const duration = video && (video.content_tm || video.duration || 0);
|
|
const watchTm = video && (video.watch_tm || 0);
|
|
// 90% 이상 시청 체크 (혹은 서버에서 completed_at 반환 시 활용)
|
|
let isCompleted = false;
|
|
if (duration > 0 && watchTm >= 0.9 * duration) {
|
|
isCompleted = true;
|
|
}
|
|
// 서버에서 completed_at 반환값 활용 (권장)
|
|
if (video && video.completed_at) {
|
|
isCompleted = true;
|
|
}
|
|
// 닫기 후 DOM 갱신
|
|
const result = await origClose(...args);
|
|
if (isCompleted && contentId) {
|
|
// 해당 카드 DOM 갱신
|
|
const item = document.querySelector('.books-item[data-video-id="' + contentId + '"]');
|
|
if (item && !item.classList.contains('books-item-completed')) {
|
|
item.classList.add('books-item-completed');
|
|
// 포스트잇이 없으면 추가 (서버 렌더 구조와 동일하게)
|
|
if (!item.querySelector('.book-postit')) {
|
|
const postitLines = Array.isArray(video && video.postit)
|
|
? video.postit
|
|
.map(function (line) { return String(line || '').trim(); })
|
|
.filter(function (line) { return line.length > 0; })
|
|
: [];
|
|
|
|
const postit = document.createElement('div');
|
|
postit.className = 'book-postit';
|
|
const title = document.createElement('strong');
|
|
title.className = 'book-postit-title';
|
|
title.textContent = 'CHECK!';
|
|
|
|
const list = document.createElement('ul');
|
|
list.className = 'book-postit-list';
|
|
(postitLines.length > 0 ? postitLines : ['영상 시청 완료']).forEach(function (line) {
|
|
const li = document.createElement('li');
|
|
li.textContent = line;
|
|
list.appendChild(li);
|
|
});
|
|
|
|
postit.appendChild(title);
|
|
postit.appendChild(list);
|
|
const infoWrap = item.querySelector('.book-info-wrap');
|
|
if (infoWrap) infoWrap.appendChild(postit);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
document.querySelectorAll('.books-list').forEach(function (list) {
|
|
list.addEventListener('click', function (event) {
|
|
const item = event.target.closest('.books-item');
|
|
if (!item || event.target.closest('.bookmark')) return;
|
|
|
|
event.preventDefault();
|
|
const id = String(item.getAttribute('data-video-id') || '').trim();
|
|
console.log('[myclass_list] Video item clicked:', {
|
|
id,
|
|
dataVideoId: item.getAttribute('data-video-id'),
|
|
videoData: window.MYCLASS_PAGE_VIDEOS.find(v => String(v.id) === String(id))
|
|
});
|
|
|
|
if (!id) return;
|
|
|
|
// 같은 학습 목표 영상만 추천 리스트에 표시 (최대 6개)
|
|
var clickedVid = null;
|
|
for (var i = 0; i < allVideosBackup.length; i++) {
|
|
if (String(allVideosBackup[i].id) === String(id) || String(allVideosBackup[i].content_id) === String(id)) {
|
|
clickedVid = allVideosBackup[i]; break;
|
|
}
|
|
}
|
|
if (clickedVid && clickedVid.goal_code) {
|
|
var sameGoalVids = allVideosBackup.filter(function(v) { return v.goal_code === clickedVid.goal_code; });
|
|
if (Array.isArray(modalManager.videos)) modalManager.videos = sameGoalVids;
|
|
if (modalManager.config && Array.isArray(modalManager.config.videos)) modalManager.config.videos = sameGoalVids;
|
|
}
|
|
|
|
if (typeof modalManager.openVideo === 'function') {
|
|
console.log('[myclass_list] Calling openVideo with:', id);
|
|
modalManager.openVideo(id);
|
|
} else if (typeof modalManager.loadVideoModal === 'function') {
|
|
console.log('[myclass_list] Calling loadVideoModal with:', id);
|
|
modalManager.loadVideoModal(id);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function initBookmarks() {
|
|
// 마이클래스 리스트 북마크 저장 처리
|
|
|
|
// 1단계: bookmarks 초기화 (MYCLASS_PAGE_VIDEOS 상태 기반)
|
|
if (Array.isArray(window.MYCLASS_PAGE_VIDEOS)) {
|
|
window.MYCLASS_PAGE_VIDEOS.forEach(function (video) {
|
|
const contentId = String(video.id || video.content_id || '').trim();
|
|
if (!contentId) return;
|
|
|
|
const checkbox = document.querySelector('.bookmark input[type="checkbox"][id*="' + contentId.replace(/[^A-Za-z0-9_-]/g, '_') + '"]');
|
|
if (checkbox) {
|
|
checkbox.checked = !!video.bookmark;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 2단계: change 이벤트 바인딩
|
|
document.querySelectorAll('.bookmark input[type="checkbox"]').forEach(function (checkbox) {
|
|
checkbox.addEventListener('change', async function () {
|
|
const label = this.closest('.bookmark');
|
|
if (!label) return;
|
|
|
|
const booksItem = label.closest('.books-item');
|
|
if (!booksItem) return;
|
|
|
|
const contentId = String(booksItem.getAttribute('data-video-id') || '').trim();
|
|
if (!contentId) return;
|
|
|
|
const isActive = this.checked ? '1' : '0';
|
|
|
|
try {
|
|
this.disabled = true;
|
|
|
|
const body = new URLSearchParams({
|
|
content_id: String(contentId),
|
|
is_active: isActive,
|
|
});
|
|
|
|
console.log('[myclass_list] 북마크 API 호출:', { content_id: contentId, is_active: isActive });
|
|
|
|
const response = await fetch('/bbs/api/save_wishlist.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
body: body.toString(),
|
|
});
|
|
|
|
const responseText = await response.text();
|
|
console.log('[myclass_list] 북마크 API 응답:', responseText);
|
|
|
|
let result;
|
|
try {
|
|
result = JSON.parse(responseText);
|
|
} catch (parseErr) {
|
|
console.error('[myclass_list] 북마크 API 응답 JSON 파싱 실패:', parseErr, responseText);
|
|
throw new Error('서버 응답을 처리할 수 없습니다.');
|
|
}
|
|
|
|
if (!response.ok || !result || result.success !== true) {
|
|
console.error('[myclass_list] 북마크 API 실패:', { status: response.status, result });
|
|
throw new Error(result?.message || '북마크 저장에 실패했습니다.');
|
|
}
|
|
|
|
console.log('[myclass_list] 북마크 저장 성공:', result);
|
|
} catch (error) {
|
|
console.error('[myclass_list] 북마크 저장 실패:', error);
|
|
this.checked = !this.checked;
|
|
alert(error?.message || '북마크 저장 중 오류가 발생했습니다.');
|
|
} finally {
|
|
this.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function initGsapScrollEvents() {
|
|
if (typeof window.gsap === 'undefined' || typeof window.ScrollTrigger === 'undefined') return false;
|
|
|
|
const main = document.querySelector('.myclass-main');
|
|
const content = document.querySelector('.myclass-content');
|
|
const sections = window.gsap.utils.toArray('.scroll-section');
|
|
const timeline = document.querySelector('.scroll-timeline');
|
|
const fill = document.querySelector('.timeline-progress-fill');
|
|
const nav = document.querySelector('.quarter-nav');
|
|
const sidebarGoals = window.gsap.utils.toArray('.sidebar-goal-item');
|
|
const quarterSelectWrap = document.querySelector('.quarter-select-wrap');
|
|
const header = document.querySelector('header');
|
|
const MO_BREAKPOINT = 1024;
|
|
|
|
if (!main || !content || !sections.length) return false;
|
|
|
|
window.gsap.registerPlugin(window.ScrollTrigger);
|
|
|
|
const dots = [];
|
|
const quarterNavItems = [];
|
|
let quarterNavFill = null;
|
|
let quarterSelect = null;
|
|
let absYList = [];
|
|
let timelineH = 0;
|
|
let currentIndex = -1;
|
|
let stInstances = [];
|
|
let layoutRefreshRaf = null;
|
|
|
|
function getHeaderHeight() {
|
|
return header ? header.offsetHeight : 0;
|
|
}
|
|
|
|
function setHeights() {
|
|
const isMo = window.innerWidth <= MO_BREAKPOINT;
|
|
if (isMo) {
|
|
content.style.height = '';
|
|
main.style.height = '';
|
|
sections.forEach(function (section) { section.style.height = ''; });
|
|
return;
|
|
}
|
|
|
|
content.style.height = '';
|
|
let h = Math.round(content.getBoundingClientRect().height || content.clientHeight);
|
|
if (!h || h < 1) {
|
|
h = window.innerHeight - getHeaderHeight();
|
|
}
|
|
main.style.height = h + 'px';
|
|
sections.forEach(function (section) { section.style.height = h + 'px'; });
|
|
}
|
|
|
|
function buildTimelineDots() {
|
|
if (!timeline) return;
|
|
timeline.querySelectorAll('.timeline-dot').forEach(function (dot) { dot.remove(); });
|
|
sections.forEach(function (section, index) {
|
|
const dot = document.createElement('div');
|
|
dot.className = 'timeline-dot' + (index === 0 ? ' is-active' : '');
|
|
dot.dataset.index = String(index);
|
|
if (!section.querySelector('.sidebar-quarter')) {
|
|
dot.classList.add('timeline-dot--triangle');
|
|
dot.innerHTML = '<span class="ico-quarter-triangle" aria-hidden="true"></span>';
|
|
}
|
|
timeline.appendChild(dot);
|
|
dots.push(dot);
|
|
});
|
|
}
|
|
|
|
function buildQuarterSelect() {
|
|
if (!quarterSelectWrap) return;
|
|
quarterSelectWrap.innerHTML = '';
|
|
const select = document.createElement('select');
|
|
select.className = 'quarter-select';
|
|
select.setAttribute('aria-label', '분기 선택');
|
|
|
|
sections.forEach(function (section, index) {
|
|
const quarter = section.dataset.quarter || String(index + 1);
|
|
const quarterEl = section.querySelector('.sidebar-quarter');
|
|
const label = quarterEl
|
|
? quarterEl.innerText.replace(/\s+/g, ' ').trim()
|
|
: quarter + '분기';
|
|
|
|
const option = document.createElement('option');
|
|
option.value = String(index);
|
|
option.textContent = label;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
select.addEventListener('change', function () {
|
|
const idx = parseInt(this.value, 10);
|
|
if (Number.isNaN(idx)) return;
|
|
main.scrollTo({ left: idx * main.offsetWidth, behavior: 'smooth' });
|
|
});
|
|
|
|
quarterSelectWrap.appendChild(select);
|
|
quarterSelect = select;
|
|
}
|
|
|
|
function buildQuarterNav() {
|
|
if (!nav) return;
|
|
nav.innerHTML = '';
|
|
|
|
const trackEl = document.createElement('div');
|
|
trackEl.className = 'quarter-nav-track';
|
|
trackEl.setAttribute('aria-hidden', 'true');
|
|
trackEl.innerHTML = '<div class="quarter-nav-fill"></div>';
|
|
nav.appendChild(trackEl);
|
|
quarterNavFill = trackEl.querySelector('.quarter-nav-fill');
|
|
|
|
sections.forEach(function (section, index) {
|
|
const quarter = section.dataset.quarter || String(index + 1);
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'quarter-nav-item' + (index === 0 ? ' is-active' : '');
|
|
button.setAttribute('aria-label', quarter + '분기로 이동');
|
|
button.innerHTML =
|
|
'<span class="quarter-nav-dot" aria-hidden="true"></span>' +
|
|
'<span class="quarter-nav-label">' + quarter + '분기</span>';
|
|
|
|
button.addEventListener('click', function () {
|
|
if (window.innerWidth <= MO_BREAKPOINT) {
|
|
main.scrollTo({ left: index * main.offsetWidth, behavior: 'smooth' });
|
|
} else {
|
|
main.scrollTo({ top: index * main.offsetHeight, behavior: 'smooth' });
|
|
}
|
|
});
|
|
|
|
nav.appendChild(button);
|
|
quarterNavItems.push(button);
|
|
});
|
|
}
|
|
|
|
function positionTimeline() {
|
|
if (!timeline || !sections.length) return;
|
|
|
|
const sectionH = main.clientHeight;
|
|
const halfDot = 6;
|
|
const savedScroll = main.scrollTop;
|
|
main.scrollTop = 0;
|
|
|
|
const mainRect = main.getBoundingClientRect();
|
|
absYList = sections.map(function (section, index) {
|
|
const anchor = section.querySelector('.sidebar-quarter') || section.querySelector('.sidebar-goal-icon');
|
|
if (!anchor) return index * sectionH;
|
|
const sectionRect = section.getBoundingClientRect();
|
|
const anchorRect = anchor.getBoundingClientRect();
|
|
const offsetInSection = anchorRect.top + anchorRect.height / 2 - sectionRect.top;
|
|
return index * sectionH + offsetInSection;
|
|
});
|
|
|
|
const sidebar = sections[0].querySelector('.scroll-section-sidebar');
|
|
const sidebarRect = sidebar ? sidebar.getBoundingClientRect() : mainRect;
|
|
const timelineLeft = (sidebarRect.left - mainRect.left) + 6;
|
|
|
|
main.scrollTop = savedScroll;
|
|
|
|
const firstY = absYList[0];
|
|
const lastY = absYList[absYList.length - 1];
|
|
timelineH = Math.max(lastY - firstY, 0);
|
|
|
|
timeline.style.top = firstY + 'px';
|
|
timeline.style.left = timelineLeft + 'px';
|
|
timeline.style.height = timelineH + 'px';
|
|
|
|
dots.forEach(function (dot, index) {
|
|
dot.style.top = (absYList[index] - firstY - halfDot) + 'px';
|
|
});
|
|
}
|
|
|
|
function positionQuarterNav() {
|
|
if (!nav || quarterNavItems.length < 2) return;
|
|
const trackEl = nav.querySelector('.quarter-nav-track');
|
|
if (!trackEl) return;
|
|
|
|
const firstDot = quarterNavItems[0].querySelector('.quarter-nav-dot');
|
|
const lastDot = quarterNavItems[quarterNavItems.length - 1].querySelector('.quarter-nav-dot');
|
|
if (!firstDot || !lastDot) return;
|
|
|
|
const navRect = nav.getBoundingClientRect();
|
|
const fRect = firstDot.getBoundingClientRect();
|
|
const lRect = lastDot.getBoundingClientRect();
|
|
|
|
if (window.innerWidth <= MO_BREAKPOINT) {
|
|
const top = fRect.top + fRect.height / 2 - navRect.top;
|
|
const left = fRect.left + fRect.width / 2 - navRect.left;
|
|
const width = (lRect.left + lRect.width / 2 - navRect.left) - left;
|
|
trackEl.style.top = top + 'px';
|
|
trackEl.style.left = left + 'px';
|
|
trackEl.style.width = Math.max(width, 0) + 'px';
|
|
trackEl.style.height = '';
|
|
} else {
|
|
const top = fRect.top + fRect.height / 2 - navRect.top;
|
|
const left = fRect.left + fRect.width / 2 - navRect.left;
|
|
const height = (lRect.top + lRect.height / 2 - navRect.top) - top;
|
|
trackEl.style.top = top + 'px';
|
|
trackEl.style.left = left + 'px';
|
|
trackEl.style.height = Math.max(height, 0) + 'px';
|
|
trackEl.style.width = '';
|
|
}
|
|
}
|
|
|
|
function updateProgress() {
|
|
const isMo = window.innerWidth <= MO_BREAKPOINT;
|
|
const sectionH = isMo ? main.clientWidth : main.clientHeight;
|
|
const scrollPos = isMo ? main.scrollLeft : main.scrollTop;
|
|
let fillPct = 0;
|
|
|
|
if (absYList.length >= 2 && timelineH > 0) {
|
|
const rawIdx = scrollPos / sectionH;
|
|
const i0 = Math.max(0, Math.min(Math.floor(rawIdx), absYList.length - 2));
|
|
const i1 = i0 + 1;
|
|
const t = rawIdx - i0;
|
|
const pos0 = absYList[i0] - absYList[0];
|
|
const pos1 = absYList[i1] - absYList[0];
|
|
const fillPx = pos0 + t * (pos1 - pos0);
|
|
fillPct = Math.min((fillPx / timelineH) * 100, 100);
|
|
}
|
|
|
|
const pct = fillPct + '%';
|
|
if (fill) fill.style.height = pct;
|
|
if (quarterNavFill) {
|
|
if (isMo) quarterNavFill.style.width = pct;
|
|
else quarterNavFill.style.height = pct;
|
|
}
|
|
}
|
|
|
|
function refreshLayoutAfterHeaderChange() {
|
|
if (layoutRefreshRaf) cancelAnimationFrame(layoutRefreshRaf);
|
|
layoutRefreshRaf = requestAnimationFrame(function () {
|
|
setHeights();
|
|
positionTimeline();
|
|
positionQuarterNav();
|
|
updateProgress();
|
|
if (typeof window.ScrollTrigger !== 'undefined') {
|
|
window.ScrollTrigger.refresh();
|
|
}
|
|
layoutRefreshRaf = null;
|
|
});
|
|
}
|
|
|
|
function updateActive(index) {
|
|
if (index === currentIndex) return;
|
|
currentIndex = index;
|
|
|
|
sidebarGoals.forEach(function (goal, i) {
|
|
goal.classList.toggle('is-active', i === index);
|
|
});
|
|
dots.forEach(function (dot, i) {
|
|
dot.classList.toggle('is-active', i <= index);
|
|
});
|
|
quarterNavItems.forEach(function (item, i) {
|
|
item.classList.toggle('is-active', i <= index);
|
|
});
|
|
|
|
if (quarterSelect && window.innerWidth <= MO_BREAKPOINT) {
|
|
quarterSelect.value = String(index);
|
|
}
|
|
|
|
const page = document.querySelector('.myclass-page');
|
|
if (page) {
|
|
const shouldHideIntro = index > 0;
|
|
const prevHidden = page.classList.contains('intro-hidden');
|
|
page.classList.toggle('intro-hidden', shouldHideIntro);
|
|
if (prevHidden !== shouldHideIntro) {
|
|
refreshLayoutAfterHeaderChange();
|
|
}
|
|
}
|
|
}
|
|
|
|
function initGSAPTriggers() {
|
|
stInstances.forEach(function (st) { st.kill(); });
|
|
stInstances = [];
|
|
window.ScrollTrigger.clearScrollMemory();
|
|
|
|
setHeights();
|
|
positionTimeline();
|
|
|
|
if (window.innerWidth <= MO_BREAKPOINT) return;
|
|
|
|
window.ScrollTrigger.defaults({ scroller: main });
|
|
sections.forEach(function (section, index) {
|
|
stInstances.push(
|
|
window.ScrollTrigger.create({
|
|
trigger: section,
|
|
start: 'top center',
|
|
end: 'bottom center',
|
|
onEnter: function () { updateActive(index); },
|
|
onEnterBack: function () { updateActive(index); },
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
buildTimelineDots();
|
|
buildQuarterSelect();
|
|
buildQuarterNav();
|
|
setHeights();
|
|
positionTimeline();
|
|
positionQuarterNav();
|
|
updateActive(0);
|
|
updateProgress();
|
|
|
|
const revealSections = document.querySelectorAll('.scroll-section-reveal');
|
|
if (revealSections.length && 'IntersectionObserver' in window) {
|
|
const observer = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
setTimeout(function () { entry.target.classList.add('is-visible'); }, 80);
|
|
observer.unobserve(entry.target);
|
|
});
|
|
}, { root: main, rootMargin: '0px', threshold: 0.15 });
|
|
revealSections.forEach(function (section) { observer.observe(section); });
|
|
} else {
|
|
revealSections.forEach(function (section) { section.classList.add('is-visible'); });
|
|
}
|
|
|
|
main.addEventListener('scroll', function () {
|
|
updateProgress();
|
|
const isMo = window.innerWidth <= MO_BREAKPOINT;
|
|
const idx = isMo
|
|
? Math.round(main.scrollLeft / main.clientWidth)
|
|
: Math.round(main.scrollTop / main.clientHeight);
|
|
updateActive(Math.min(idx, sections.length - 1));
|
|
}, { passive: true });
|
|
|
|
let swipeStartX = 0;
|
|
let swipeStartY = 0;
|
|
main.addEventListener('touchstart', function (event) {
|
|
if (window.innerWidth > MO_BREAKPOINT) return;
|
|
swipeStartX = event.touches[0].clientX;
|
|
swipeStartY = event.touches[0].clientY;
|
|
}, { passive: true });
|
|
|
|
main.addEventListener('touchend', function (event) {
|
|
if (window.innerWidth > MO_BREAKPOINT) return;
|
|
const dx = event.changedTouches[0].clientX - swipeStartX;
|
|
const dy = event.changedTouches[0].clientY - swipeStartY;
|
|
const hasHorizontalSwipe = Math.abs(dx) >= 40 && Math.abs(dx) >= Math.abs(dy);
|
|
let idx = Math.round(main.scrollLeft / main.clientWidth);
|
|
|
|
if (hasHorizontalSwipe) {
|
|
if (dx < 0) idx = Math.min(idx + 1, sections.length - 1);
|
|
else idx = Math.max(idx - 1, 0);
|
|
main.scrollTo({ left: idx * main.clientWidth, behavior: 'smooth' });
|
|
}
|
|
|
|
setTimeout(function () {
|
|
const syncIdx = Math.round(main.scrollLeft / main.clientWidth);
|
|
currentIndex = -1;
|
|
updateActive(Math.min(syncIdx, sections.length - 1));
|
|
}, hasHorizontalSwipe ? 350 : 200);
|
|
}, { passive: true });
|
|
|
|
window.addEventListener('load', function () {
|
|
setHeights();
|
|
positionTimeline();
|
|
positionQuarterNav();
|
|
initGSAPTriggers();
|
|
updateProgress();
|
|
});
|
|
|
|
let resizeTimer = null;
|
|
let prevIsMo = window.innerWidth <= MO_BREAKPOINT;
|
|
let prevMainHeight = Math.round(main.getBoundingClientRect().height || 0);
|
|
window.addEventListener('resize', function () {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(function () {
|
|
const nextIsMo = window.innerWidth <= MO_BREAKPOINT;
|
|
|
|
setHeights();
|
|
positionTimeline();
|
|
positionQuarterNav();
|
|
updateProgress();
|
|
|
|
const nextMainHeight = Math.round(main.getBoundingClientRect().height || 0);
|
|
const shouldRefresh = (nextIsMo !== prevIsMo) || (Math.abs(nextMainHeight - prevMainHeight) > 1);
|
|
if (shouldRefresh && typeof window.ScrollTrigger !== 'undefined') {
|
|
window.ScrollTrigger.refresh();
|
|
}
|
|
|
|
prevIsMo = nextIsMo;
|
|
prevMainHeight = nextMainHeight;
|
|
}, 120);
|
|
});
|
|
|
|
initGSAPTriggers();
|
|
return true;
|
|
}
|
|
|
|
function initQuarterNavigation() {
|
|
const sections = Array.from(document.querySelectorAll('.scroll-section'));
|
|
const navs = Array.from(document.querySelectorAll('.quarter-nav'));
|
|
const main = document.querySelector('.myclass-main');
|
|
|
|
if (!sections.length || !navs.length || !main) return;
|
|
|
|
navs.forEach(function (nav) {
|
|
nav.innerHTML = '';
|
|
sections.forEach(function (section, index) {
|
|
const label = section.querySelector('.sidebar-quarter strong:last-child');
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'quarter-nav-item' + (index === 0 ? ' is-active' : '');
|
|
button.innerHTML = '<span class="quarter-nav-dot" aria-hidden="true"></span><span class="quarter-nav-label">' + (label ? label.textContent : String(index + 1)) + '분기</span>';
|
|
button.addEventListener('click', function () {
|
|
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
nav.appendChild(button);
|
|
});
|
|
});
|
|
|
|
function updateActiveQuarter() {
|
|
const mainTop = main.getBoundingClientRect().top;
|
|
let activeIndex = 0;
|
|
|
|
sections.forEach(function (section, index) {
|
|
const rect = section.getBoundingClientRect();
|
|
if (rect.top - mainTop <= 120) {
|
|
activeIndex = index;
|
|
}
|
|
});
|
|
|
|
navs.forEach(function (nav) {
|
|
nav.querySelectorAll('.quarter-nav-item').forEach(function (item, index) {
|
|
item.classList.toggle('is-active', index === activeIndex);
|
|
});
|
|
});
|
|
}
|
|
|
|
main.addEventListener('scroll', updateActiveQuarter, { passive: true });
|
|
window.addEventListener('resize', updateActiveQuarter);
|
|
updateActiveQuarter();
|
|
}
|
|
|
|
function initTimeline() {
|
|
const timeline = document.querySelector('.scroll-timeline');
|
|
const main = document.querySelector('.myclass-main');
|
|
const sections = Array.from(document.querySelectorAll('.scroll-section'));
|
|
const fill = document.querySelector('.timeline-progress-fill');
|
|
|
|
if (!timeline || !main || !sections.length || !fill) return;
|
|
|
|
const dots = [];
|
|
|
|
sections.forEach(function (_section, index) {
|
|
const dot = document.createElement('div');
|
|
dot.className = 'timeline-dot' + (index === 0 ? ' is-active' : '');
|
|
timeline.appendChild(dot);
|
|
dots.push(dot);
|
|
});
|
|
|
|
function position() {
|
|
const firstRect = sections[0].getBoundingClientRect();
|
|
const lastRect = sections[sections.length - 1].getBoundingClientRect();
|
|
const mainRect = main.getBoundingClientRect();
|
|
const start = firstRect.top - mainRect.top + 80;
|
|
const end = lastRect.top - mainRect.top + 80;
|
|
const height = Math.max(end - start, 0);
|
|
|
|
timeline.style.top = start + 'px';
|
|
timeline.style.height = height + 'px';
|
|
|
|
dots.forEach(function (dot, index) {
|
|
const sectionRect = sections[index].getBoundingClientRect();
|
|
const offset = sectionRect.top - mainRect.top + 80 - start;
|
|
dot.style.top = offset + 'px';
|
|
});
|
|
}
|
|
|
|
function update() {
|
|
const scrollMax = Math.max(main.scrollHeight - main.clientHeight, 1);
|
|
const ratio = Math.min(main.scrollTop / scrollMax, 1);
|
|
fill.style.height = ratio * 100 + '%';
|
|
|
|
let activeIndex = 0;
|
|
sections.forEach(function (section, index) {
|
|
if (section.offsetTop - main.scrollTop <= 120) {
|
|
activeIndex = index;
|
|
}
|
|
});
|
|
|
|
dots.forEach(function (dot, index) {
|
|
dot.classList.toggle('is-active', index <= activeIndex);
|
|
});
|
|
}
|
|
|
|
main.addEventListener('scroll', update, { passive: true });
|
|
window.addEventListener('resize', function () {
|
|
position();
|
|
update();
|
|
});
|
|
|
|
position();
|
|
update();
|
|
}
|
|
|
|
initLottieIcons();
|
|
initRankToggle();
|
|
initButtons();
|
|
initVideoModal();
|
|
initBookmarks();
|
|
if (!initGsapScrollEvents()) {
|
|
ensureSectionRevealVisible();
|
|
initQuarterNavigation();
|
|
initTimeline();
|
|
}
|
|
});
|