SHA256
Доработать UI уведомлений
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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.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');
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user