function resolveElement(value) { return typeof value === 'function' ? value() : value; } function buildArrowIcon() { const icon = document.createElement('span'); icon.className = 'scroll-to-bottom-btn__icon'; icon.setAttribute('aria-hidden', 'true'); icon.textContent = '↓'; return icon; } export function attachScrollToBottomButton({ scrollContainer, mountTarget = document.querySelector('.app-shell'), thresholdPx = 160, title = 'Вниз', } = {}) { const target = resolveElement(mountTarget); if (!(target instanceof Element)) { return { button: null, refresh() {}, cleanup() {}, }; } const button = document.createElement('button'); button.type = 'button'; button.className = 'scroll-to-bottom-btn'; button.title = title; button.setAttribute('aria-label', 'Прокрутить ленту вниз'); button.tabIndex = -1; button.append(buildArrowIcon()); target.append(button); let disposed = false; let boundContainer = null; let resizeObserver = null; let mutationObserver = null; let refreshFrame = 0; const hide = () => { button.classList.remove('is-visible'); button.setAttribute('aria-hidden', 'true'); button.tabIndex = -1; }; const scheduleRefresh = () => { if (disposed || refreshFrame) return; refreshFrame = window.requestAnimationFrame(() => { refreshFrame = 0; refresh(); }); }; const unbindContainer = () => { boundContainer?.removeEventListener('scroll', scheduleRefresh); resizeObserver?.disconnect(); mutationObserver?.disconnect(); resizeObserver = null; mutationObserver = null; boundContainer = null; }; const bindContainer = () => { const nextContainer = resolveElement(scrollContainer); if (!(nextContainer instanceof Element)) { if (boundContainer) unbindContainer(); return null; } if (nextContainer === boundContainer) return boundContainer; unbindContainer(); boundContainer = nextContainer; boundContainer.addEventListener('scroll', scheduleRefresh, { passive: true }); if (typeof ResizeObserver === 'function') { resizeObserver = new ResizeObserver(scheduleRefresh); resizeObserver.observe(boundContainer); } if (typeof MutationObserver === 'function') { mutationObserver = new MutationObserver(scheduleRefresh); mutationObserver.observe(boundContainer, { childList: true, subtree: true }); } return boundContainer; }; function refresh() { if (disposed) return; const container = bindContainer(); if (!container) { hide(); return; } const scrollHeight = Number(container.scrollHeight || 0); const clientHeight = Number(container.clientHeight || 0); const scrollTop = Number(container.scrollTop || 0); const maxScrollTop = Math.max(0, scrollHeight - clientHeight); const distanceToBottom = Math.max(0, maxScrollTop - scrollTop); const shouldShow = maxScrollTop > 8 && distanceToBottom > Math.max(24, Number(thresholdPx || 0)); button.classList.toggle('is-visible', shouldShow); button.setAttribute('aria-hidden', shouldShow ? 'false' : 'true'); button.tabIndex = shouldShow ? 0 : -1; } const scrollToBottom = () => { const container = bindContainer(); if (!container) return; if (typeof container.scrollTo === 'function') { container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); } else { container.scrollTop = container.scrollHeight; } scheduleRefresh(); }; button.addEventListener('click', scrollToBottom); window.addEventListener('resize', scheduleRefresh); window.visualViewport?.addEventListener('resize', scheduleRefresh); window.requestAnimationFrame(refresh); return { button, refresh: scheduleRefresh, cleanup() { if (disposed) return; disposed = true; if (refreshFrame) { window.cancelAnimationFrame(refreshFrame); refreshFrame = 0; } unbindContainer(); window.removeEventListener('resize', scheduleRefresh); window.visualViewport?.removeEventListener('resize', scheduleRefresh); button.removeEventListener('click', scrollToBottom); button.remove(); }, }; }