const getCssBreakpoint = (propertyName, fallback) => ( getComputedStyle(document.documentElement).getPropertyValue(propertyName).trim() || fallback ); const compactMediaQuery = window.matchMedia( `(max-width: ${getCssBreakpoint('--breakpoint-compact', '930px')})` ); const headerMenuMediaQuery = window.matchMedia( `(max-width: ${getCssBreakpoint('--breakpoint-header-menu', '1340px')})` ); const openOverlays = new Set(); let lockedPageScrollY = 0; let lockedBodyStyles; function lockPageScroll() { if (lockedBodyStyles) return; lockedPageScrollY = window.scrollY; lockedBodyStyles = { position: document.body.style.position, top: document.body.style.top, right: document.body.style.right, left: document.body.style.left, width: document.body.style.width, overflow: document.body.style.overflow }; Object.assign(document.body.style, { position: 'fixed', top: `-${lockedPageScrollY}px`, right: '0', left: '0', width: '100%', overflow: 'hidden' }); } function unlockPageScroll() { if (!lockedBodyStyles) return; const previousBodyStyles = lockedBodyStyles; lockedBodyStyles = null; Object.assign(document.body.style, previousBodyStyles); const previousScrollBehavior = document.documentElement.style.scrollBehavior; document.documentElement.style.scrollBehavior = 'auto'; window.scrollTo(0, lockedPageScrollY); document.documentElement.style.scrollBehavior = previousScrollBehavior; } document.addEventListener('DOMContentLoaded', () => { initPageLoader(); initSectionReveals(); initHeader(); initFeature(); initProtein(); initFaq(); initFooterReveal(); initModals(); initNutritionDialog(); initScaledFigures(); }); function initPageLoader() { const body = document.body; const loader = document.querySelector('.page-loader'); if (!loader) return; body.classList.add('is-loading'); const delay = (duration) => new Promise((resolve) => { window.setTimeout(resolve, duration); }); const waitForImage = (image) => { if (image.complete) return Promise.resolve(); return new Promise((resolve) => { image.addEventListener('load', resolve, { once: true }); image.addEventListener('error', resolve, { once: true }); }); }; const criticalImages = [...document.querySelectorAll('.kv img')]; const criticalResources = Promise.allSettled([ document.fonts?.ready ?? Promise.resolve(), ...criticalImages.map(waitForImage) ]); const minimumDuration = delay(600); const maximumDuration = delay(4000); const readyToShow = Promise.all([minimumDuration, criticalResources]); Promise.race([readyToShow, maximumDuration]).then(() => { loader.classList.add('is-hide'); body.classList.remove('is-loading'); const removeLoader = () => loader.remove(); loader.addEventListener('transitionend', removeLoader, { once: true }); window.setTimeout(removeLoader, 500); }); } function initScaledFigures() { const figures = document.querySelectorAll('[data-scaled-figure]'); if (!figures.length) return; const update = (figure) => { const scale = figure.getBoundingClientRect().width / 500; if (scale <= 0) return; figure.style.setProperty('--figure-font-size', `${16 * scale}px`); figure.style.setProperty('--figure-sup-size', `${10 * scale}px`); }; if (!('ResizeObserver' in window)) { figures.forEach(update); window.addEventListener('resize', () => figures.forEach(update), { passive: true }); return; } const observer = new ResizeObserver((entries) => { entries.forEach((entry) => update(entry.target)); }); figures.forEach((figure) => { update(figure); observer.observe(figure); }); } function initSectionReveals() { const sections = [...document.querySelectorAll('[data-reveal]')]; if (!('IntersectionObserver' in window)) { sections.forEach((section) => section.classList.add('is-revealed')); return; } const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add('is-revealed'); observer.unobserve(entry.target); }); }, { rootMargin: '0px 0px -12% 0px', threshold: 0.12 }); sections.forEach((section) => observer.observe(section)); } function initFeature() { initFeatureAccordions(); initFeatureSlider(); } function setPageOverlayState(overlay, isOpen) { if (!overlay) return; if (isOpen) { openOverlays.add(overlay); } else { openOverlays.delete(overlay); } if (openOverlays.size) { lockPageScroll(); } else { unlockPageScroll(); } } function createAccordionGroup({ items, triggerSelector, panelSelector, canToggle = () => true }) { const setItemState = (item, isOpen, updateClass = true) => { const trigger = item.querySelector(triggerSelector); const panel = item.querySelector(panelSelector); if (updateClass) item.classList.toggle('is-open', isOpen); trigger?.setAttribute('aria-expanded', String(isOpen)); panel?.setAttribute('aria-hidden', String(!isOpen)); }; items.forEach((item) => { item.querySelector(triggerSelector)?.addEventListener('click', () => { if (!canToggle()) return; const shouldOpen = !item.classList.contains('is-open'); items.forEach((accordion) => { setItemState(accordion, shouldOpen && accordion === item); }); }); }); return { setItemState }; } function createSlider({ root, viewport, slides, tabs = [], pages = [], previousButton, nextButton, mobileQuery, onUpdate, onResize }) { if (!root || !viewport || !slides.length) return null; let activeIndex = Math.max( 0, slides.findIndex((slide) => slide.classList.contains('is-active')) ); let scrollFrame; let programmaticScrollTimer; let isProgrammaticScroll = false; const update = (index, shouldScroll = true) => { activeIndex = Math.max(0, Math.min(index, slides.length - 1)); slides.forEach((slide, slideIndex) => { slide.classList.toggle('is-active', slideIndex === activeIndex); slide.classList.toggle('is-before', slideIndex < activeIndex); slide.classList.toggle('is-after', slideIndex > activeIndex); }); tabs.forEach((tab, tabIndex) => { const isActive = tabIndex === activeIndex; tab.classList.toggle('is-active', isActive); tab.setAttribute('aria-selected', String(isActive)); }); pages.forEach((page, pageIndex) => { page.classList.toggle('is-active', pageIndex === activeIndex); }); if (previousButton) previousButton.disabled = activeIndex === 0; if (nextButton) nextButton.disabled = activeIndex === slides.length - 1; onUpdate?.(activeIndex); if (!mobileQuery.matches) { viewport.scrollLeft = 0; return; } if (!shouldScroll) return; isProgrammaticScroll = true; clearTimeout(programmaticScrollTimer); viewport.scrollTo({ left: Math.max(0, slides[activeIndex].offsetLeft - (viewport.clientWidth - slides[activeIndex].offsetWidth) / 2), behavior: 'smooth' }); programmaticScrollTimer = window.setTimeout(() => { isProgrammaticScroll = false; }, 600); }; tabs.forEach((tab, index) => tab.addEventListener('click', () => update(index))); pages.forEach((page, index) => page.addEventListener('click', () => update(index))); previousButton?.addEventListener('click', () => update(activeIndex - 1)); nextButton?.addEventListener('click', () => update(activeIndex + 1)); viewport.addEventListener('scroll', () => { if (!mobileQuery.matches || isProgrammaticScroll) return; cancelAnimationFrame(scrollFrame); scrollFrame = requestAnimationFrame(() => { const viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2; const nextIndex = slides.reduce((closestIndex, slide, slideIndex) => { const currentDistance = Math.abs(slide.offsetLeft + slide.offsetWidth / 2 - viewportCenter); const closestDistance = Math.abs(slides[closestIndex].offsetLeft + slides[closestIndex].offsetWidth / 2 - viewportCenter); return currentDistance < closestDistance ? slideIndex : closestIndex; }, 0); if (nextIndex !== activeIndex) update(nextIndex, false); }); }, { passive: true }); viewport.addEventListener('scrollend', () => { isProgrammaticScroll = false; clearTimeout(programmaticScrollTimer); }); const syncLayout = () => { onResize?.(); update(activeIndex, false); }; mobileQuery.addEventListener('change', syncLayout); update(activeIndex, false); return { update }; } function initProtein() { const mobileQuery = compactMediaQuery; document.querySelectorAll('[data-protein-slider]').forEach((section) => { const viewport = section.querySelector('.protein-slider__viewport'); const slides = [...section.querySelectorAll('[data-protein-slide]')]; const tabs = [...section.querySelectorAll('[data-protein-tab]')]; const pages = [...section.querySelectorAll('[data-protein-page]')]; const fixedLabel = section.querySelector('[data-protein-label]'); const mobileLabels = slides.map((slide) => slide.querySelector('.protein-chart__mobile-label')).filter(Boolean); let labelResizeFrame; let observedSectionWidth; const syncChartOverflow = () => { slides.forEach((slide) => { slide.classList.toggle( 'is-scrollable', !mobileQuery.matches && slide.scrollHeight > slide.clientHeight + 2 ); }); }; const syncMobileLabelHeights = () => { mobileLabels.forEach((label) => { label.style.minHeight = ''; }); }; createSlider({ root: section, viewport, slides, tabs, pages, previousButton: section.querySelector('[data-protein-prev]'), nextButton: section.querySelector('[data-protein-next]'), mobileQuery, onUpdate: (activeIndex) => { tabs.forEach((tab, tabIndex) => { const isActive = tabIndex === activeIndex; const icon = tab.querySelector('img'); if (icon) icon.src = isActive ? 'https://www.nestlehealthscience.com.tw/sites/default/files/2026-11/261106peptamen-ic-check2-active.png' : 'https://www.nestlehealthscience.com.tw/sites/default/files/2026-11/261106peptamen-ic-check2-active.png'; }); if (fixedLabel) fixedLabel.textContent = slides[activeIndex].dataset.chartLabel || ''; }, onResize: () => { syncMobileLabelHeights(); syncChartOverflow(); } }); slides.forEach((slide) => { const image = slide.querySelector('.protein-chart__image'); if (image && !image.complete) image.addEventListener('load', syncChartOverflow, { once: true }); }); if ('ResizeObserver' in window) { const labelResizeObserver = new ResizeObserver(([entry]) => { const nextWidth = entry.contentRect.width; if (nextWidth === observedSectionWidth) return; observedSectionWidth = nextWidth; cancelAnimationFrame(labelResizeFrame); labelResizeFrame = requestAnimationFrame(() => { syncMobileLabelHeights(); syncChartOverflow(); }); }); labelResizeObserver.observe(section); } document.fonts?.ready.then(syncMobileLabelHeights); requestAnimationFrame(() => { syncMobileLabelHeights(); syncChartOverflow(); }); }); const dialog = document.querySelector('[data-chart-dialog]'); const dialogFigure = dialog?.querySelector('[data-chart-dialog-figure]'); const dialogImage = dialog?.querySelector('[data-chart-dialog-image]'); const dialogLabel = dialog?.querySelector('[data-chart-dialog-label]'); const dialogPage = dialog?.querySelector('[data-chart-dialog-page]'); const dialogPreviousButton = dialog?.querySelector('[data-chart-dialog-prev]'); const dialogNextButton = dialog?.querySelector('[data-chart-dialog-next]'); let dialogItems = []; let dialogIndex = 0; let dialogCloseTimer; const getDialogItem = (slide) => ({ src: slide?.querySelector('.protein-chart__image')?.src || '', alt: slide?.querySelector('.protein-chart__image')?.alt || '', label: slide?.dataset.chartLabel || slide?.querySelector('.protein-chart__label')?.textContent || '', hasImageCopy: Boolean(slide?.querySelector('.protein-chart__image-copy')) }); const updateDialog = (index) => { if (!dialogItems.length || !dialogImage || !dialogLabel || !dialogPage) return; dialogIndex = Math.max(0, Math.min(index, dialogItems.length - 1)); const item = dialogItems[dialogIndex]; dialogImage.src = item.src; dialogImage.alt = item.alt; dialog.classList.toggle('has-image-copy', item.hasImageCopy); dialogFigure?.setAttribute('aria-label', item.hasImageCopy ? '穀胱甘肽是體內最重要的抗氧化劑以及自由基掃除劑,維持細胞內氧化壓力的平衡8' : item.alt); dialogLabel.textContent = item.label; dialogPage.textContent = `${dialogIndex + 1} / ${dialogItems.length}`; if (dialogPreviousButton) dialogPreviousButton.disabled = dialogIndex === 0; if (dialogNextButton) dialogNextButton.disabled = dialogIndex === dialogItems.length - 1; }; const closeDialog = () => { if (!dialog?.open || dialog.classList.contains('is-closing')) return; dialog.classList.add('is-closing'); clearTimeout(dialogCloseTimer); dialogCloseTimer = window.setTimeout(() => dialog.close(), 200); }; document.querySelectorAll('[data-chart-zoom]').forEach((button) => { button.addEventListener('click', () => { const slider = button.closest('[data-protein-slider]'); const selectedSlide = button.closest('.protein-chart') ?? slider?.querySelector('.protein-chart.is-active'); if (!dialog || !selectedSlide) return; const slides = slider?.hasAttribute('data-chart-gallery') ? [...slider.querySelectorAll('[data-protein-slide]')] : [selectedSlide]; dialogItems = slides.map(getDialogItem).filter((item) => item.src); dialogIndex = Math.max(0, slides.indexOf(selectedSlide)); dialog.classList.toggle('is-gallery', dialogItems.length > 1); dialog.classList.remove('is-plain'); updateDialog(dialogIndex); dialog.showModal(); setPageOverlayState(dialog, true); }); }); document.querySelectorAll('[data-feature-dialog-src]').forEach((button) => { button.addEventListener('click', () => { if (!dialog || !dialogImage || !dialogLabel || !dialogPage) return; dialogItems = [{ src: button.dataset.featureDialogSrc || '', alt: button.dataset.featureDialogAlt || '', label: '' }]; dialogIndex = 0; dialog.classList.remove('is-gallery'); dialog.classList.add('is-plain'); updateDialog(0); dialog.showModal(); setPageOverlayState(dialog, true); }); }); dialogPreviousButton?.addEventListener('click', () => updateDialog(dialogIndex - 1)); dialogNextButton?.addEventListener('click', () => updateDialog(dialogIndex + 1)); dialog?.querySelector('[data-chart-close]')?.addEventListener('click', closeDialog); dialog?.addEventListener('click', (event) => { if (event.target === dialog) closeDialog(); }); dialog?.addEventListener('cancel', (event) => { event.preventDefault(); closeDialog(); }); dialog?.addEventListener('close', () => { clearTimeout(dialogCloseTimer); dialog.classList.remove('is-closing'); setPageOverlayState(dialog, false); }); } function initFeatureAccordions() { const cards = [...document.querySelectorAll('.feature-info-card')]; const mobileQuery = compactMediaQuery; const accordion = createAccordionGroup({ items: cards, triggerSelector: '.feature-info-card__trigger', panelSelector: '.feature-info-card__panel', canToggle: () => mobileQuery.matches }); const syncAccordionState = () => { cards.forEach((card) => { const isOpen = mobileQuery.matches ? card.classList.contains('is-open') : true; accordion.setItemState(card, isOpen, false); }); }; mobileQuery.addEventListener('change', syncAccordionState); syncAccordionState(); } function initFeatureSlider() { const slider = document.querySelector('[data-feature-slider]'); if (!slider) return; const root = slider.closest('.feature-evidence__inner') ?? slider; const viewport = slider.querySelector('.feature-slider__viewport'); const track = slider.querySelector('.feature-slider__track'); const slides = [...slider.querySelectorAll('[data-feature-slide]')]; const tabs = [...root.querySelectorAll('[data-feature-tab]')]; const pages = [...slider.querySelectorAll('[data-feature-page]')]; const mobileQuery = compactMediaQuery; createSlider({ root, viewport, slides, tabs, pages, previousButton: slider.querySelector('[data-feature-prev]'), nextButton: slider.querySelector('[data-feature-next]'), mobileQuery, onResize: () => { if (mobileQuery.matches) track.style.transform = ''; } }); viewport.scrollLeft = 0; } function initHeader() { const body = document.body; const header = document.querySelector('.site-header'); const menuButton = document.querySelector('.menu-btn'); const desktopNav = document.querySelector('.desktop-nav'); const responsiveMenu = document.querySelector('.responsive-menu'); document.querySelectorAll('[data-nav-copy]').forEach((nav) => { nav.innerHTML = desktopNav?.innerHTML ?? ''; }); const navLinks = document.querySelectorAll( '.desktop-nav a, .responsive-menu__nav a' ); const setMenuState = (isOpen) => { const canOpen = Boolean(isOpen && responsiveMenu); body.classList.toggle('menu-open', canOpen); menuButton?.setAttribute('aria-expanded', String(canOpen)); menuButton?.setAttribute('aria-label', canOpen ? '關閉選單' : '開啟選單'); responsiveMenu?.setAttribute('aria-hidden', String(!canOpen)); }; const closeMenu = () => setMenuState(false); menuButton?.addEventListener('click', () => { setMenuState(!body.classList.contains('menu-open')); }); responsiveMenu?.querySelectorAll('a[href^="#"]').forEach((link) => { link.addEventListener('click', (event) => { const target = document.querySelector(link.hash); closeMenu(); if (!target) return; event.preventDefault(); history.pushState(null, '', link.hash); const imagesBeforeTarget = [...document.images].filter((image) => ( !image.complete && Boolean(image.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_FOLLOWING) )); const imageSettled = imagesBeforeTarget.map((image) => { image.loading = 'eager'; return new Promise((resolve) => { image.addEventListener('load', resolve, { once: true }); image.addEventListener('error', resolve, { once: true }); }); }); const layoutSettled = Promise.allSettled([ document.fonts?.ready ?? Promise.resolve(), ...imageSettled ]); const timeout = new Promise((resolve) => { window.setTimeout(resolve, 1200); }); requestAnimationFrame(() => { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); Promise.race([layoutSettled, timeout]).then(() => { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); window.setTimeout(() => { target.scrollIntoView({ behavior: 'auto', block: 'start' }); }, 450); }); }); }); document.addEventListener('click', (event) => { if (!body.classList.contains('menu-open')) return; const clickedHeader = header?.contains(event.target); const clickedMenu = responsiveMenu?.contains(event.target); if (!clickedHeader && !clickedMenu) closeMenu(); }); headerMenuMediaQuery.addEventListener('change', closeMenu); initHeaderScrollState(header); initActiveNavigation(navLinks); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeMenu(); }); } function initHeaderScrollState(header) { const intro = document.querySelector('.kv'); if (!header || !intro) return; let frame = 0; const update = () => { frame = 0; const rootStyle = getComputedStyle(document.documentElement); const headerTop = parseFloat(rootStyle.getPropertyValue('--header-top')) || 0; const headerHeight = parseFloat(rootStyle.getPropertyValue('--header-height')) || 0; const touchesHero = intro.getBoundingClientRect().bottom <= headerTop + headerHeight; header.classList.toggle('is-scrolled', touchesHero); }; const requestUpdate = () => { if (frame) return; frame = requestAnimationFrame(update); }; update(); window.addEventListener('scroll', requestUpdate, { passive: true }); window.addEventListener('resize', requestUpdate, { passive: true }); } function initActiveNavigation(navLinks) { const sectionIds = ['quality', 'feature', 'audience', 'faq', 'channel']; const sections = sectionIds .map((id) => document.getElementById(id)) .filter(Boolean); let frame = 0; const setActiveNav = (activeId) => { navLinks.forEach((link) => { const isActive = Boolean(activeId) && link.hash === `#${activeId}`; link.classList.toggle('active', isActive); if (isActive) { link.setAttribute('aria-current', 'true'); } else { link.removeAttribute('aria-current'); } }); }; setActiveNav(null); if (!sections.length) return; const update = () => { frame = 0; const marker = window.innerHeight * .3; const activeSection = sections.reduce((current, section) => ( section.getBoundingClientRect().top <= marker ? section : current ), null); setActiveNav(activeSection?.id ?? null); }; const requestUpdate = () => { if (frame) return; frame = requestAnimationFrame(update); }; update(); window.addEventListener('scroll', requestUpdate, { passive: true }); window.addEventListener('resize', requestUpdate, { passive: true }); } function initFaq() { const accordions = [...document.querySelectorAll('.faq__item')]; createAccordionGroup({ items: accordions, triggerSelector: '.faq__trigger', panelSelector: '.faq__panel' }); } function initFooterReveal() { const footer = document.querySelector('.site-footer'); if (!footer) return; if (!('IntersectionObserver' in window)) { footer.classList.add('is-active'); return; } const observer = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) return; footer.classList.add('is-active'); observer.disconnect(); }, { threshold: 0.2 }); observer.observe(footer); } function initModals() { const setModalState = (modal, isOpen) => { if (!modal) return; modal.classList.toggle('is-open', isOpen); modal.setAttribute('aria-hidden', String(!isOpen)); setPageOverlayState(modal, isOpen); }; document.querySelectorAll('[data-modal-open]').forEach((button) => { button.addEventListener('click', () => { setModalState(document.getElementById(button.dataset.modalOpen), true); }); }); document.querySelectorAll('[data-modal-close]').forEach((button) => { button.addEventListener('click', () => { setModalState(button.closest('.modal'), false); }); }); document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') return; document.querySelectorAll('.modal.is-open').forEach((modal) => { setModalState(modal, false); }); }); } function initNutritionDialog() { const dialog = document.querySelector('[data-nutrition-dialog]'); const scroller = dialog?.querySelector('[data-nutrition-scroller]'); const table = dialog?.querySelector('.nutrition-table'); const freezeShadow = dialog?.querySelector('[data-nutrition-freeze-shadow]'); let closeTimer; if (!dialog || !scroller) return; const updateScrollState = () => { const firstCell = table?.querySelector('thead th'); dialog.classList.toggle('is-scrolled-x', scroller.scrollLeft > 1); if (!table || !freezeShadow || !firstCell) return; const stickyTop = compactMediaQuery.matches ? 50 : 0; const tableTop = table.offsetTop - scroller.scrollTop; const tableBottom = tableTop + table.offsetHeight; const shadowTop = Math.max(stickyTop, tableTop); const shadowBottom = Math.min(scroller.clientHeight, tableBottom); freezeShadow.style.left = `${firstCell.offsetWidth}px`; freezeShadow.style.top = `${shadowTop}px`; freezeShadow.style.height = `${Math.max(0, shadowBottom - shadowTop)}px`; }; const close = () => { if (!dialog.open || dialog.classList.contains('is-closing')) return; dialog.classList.add('is-closing'); clearTimeout(closeTimer); closeTimer = window.setTimeout(() => dialog.close(), 250); }; document.querySelectorAll('[data-nutrition-open]').forEach((button) => { button.addEventListener('click', () => { dialog.classList.remove('is-closing'); dialog.showModal(); scroller.scrollTo({ top: 0, left: 0 }); dialog.classList.remove('is-scrolled-x'); setPageOverlayState(dialog, true); requestAnimationFrame(updateScrollState); }); }); dialog.querySelectorAll('[data-nutrition-close]').forEach((button) => { button.addEventListener('click', close); }); scroller.addEventListener('scroll', updateScrollState, { passive: true }); window.addEventListener('resize', updateScrollState); dialog.addEventListener('click', (event) => { if (event.target === dialog) close(); }); dialog.addEventListener('cancel', (event) => { event.preventDefault(); close(); }); dialog.addEventListener('close', () => { clearTimeout(closeTimer); dialog.classList.remove('is-closing', 'is-scrolled-x'); setPageOverlayState(dialog, false); }); }