- Created reusable utility modules:
* static/js/utils/stringUtils.js - string operations (esc, norm, escAttr, truncate, getInitials)
* static/js/utils/timing.js - timing utilities (debounce, throttle, delay, poll, once)
* static/js/utils/scroll.js - scroll helpers (smoothScrollTo, scrollToTop, isElementInViewport, etc.)
- Updated static/js/utils/index.js:
* Exports all utilities from a single entry point
* Provides centralized access to all utility functions
- Modified base.html:
* Removed inline esc() and norm() function definitions
* Added ES6 module imports for utilities
* Exported utilities to window for backward compatibility
* All existing code continues to work without changes
Benefits:
- Eliminates code duplication (esc/norm found in 9 templates)
- Centralizes utility functions for easier maintenance
- Enables future improvements (testing, optimization)
- Maintains backward compatibility with existing templates
Tests: All 12 static file tests passing ✅
165 lines
5.5 KiB
JavaScript
165 lines
5.5 KiB
JavaScript
/**
|
||
* Scroll Utilities
|
||
*
|
||
* Утилиты для работы с прокруткой страницы
|
||
*
|
||
* Использование:
|
||
* import { smoothScrollTo, scrollToTop } from '{% static "js/utils/scroll.js" %}';
|
||
*/
|
||
|
||
/**
|
||
* Плавная прокрутка к элементу с учётом фиксированного navbar
|
||
*
|
||
* @param {HTMLElement|string} target - элемент или селектор для прокрутки
|
||
* @param {Object} options - опции прокрутки
|
||
* @param {number} options.offset - дополнительный отступ сверху (по умолчанию 20)
|
||
* @param {string} options.behavior - тип прокрутки ('smooth' или 'auto')
|
||
* @returns {void}
|
||
*
|
||
* @example
|
||
* // Прокрутка к элементу
|
||
* smoothScrollTo('#section2');
|
||
*
|
||
* // С дополнительным отступом
|
||
* smoothScrollTo(document.querySelector('.target'), { offset: 50 });
|
||
*/
|
||
export function smoothScrollTo(target, options = {}) {
|
||
const element = typeof target === 'string'
|
||
? document.querySelector(target)
|
||
: target;
|
||
|
||
if (!element) {
|
||
console.warn('smoothScrollTo: element not found', target);
|
||
return;
|
||
}
|
||
|
||
const {
|
||
offset = 20,
|
||
behavior = 'smooth'
|
||
} = options;
|
||
|
||
// Высота фиксированного navbar (если есть)
|
||
const navbar = document.querySelector('.app-navbar, .navbar-fixed-top, .fixed-top');
|
||
const navbarHeight = navbar?.offsetHeight || 0;
|
||
|
||
// Позиция элемента относительно верха документа
|
||
const elementTop = element.getBoundingClientRect().top + window.pageYOffset;
|
||
|
||
// Финальная позиция с учётом navbar и offset
|
||
const scrollToPosition = elementTop - navbarHeight - offset;
|
||
|
||
window.scrollTo({
|
||
top: scrollToPosition,
|
||
behavior: behavior
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Прокрутка в начало страницы
|
||
*
|
||
* @param {boolean} smooth - плавная прокрутка (по умолчанию true)
|
||
* @returns {void}
|
||
*
|
||
* @example
|
||
* scrollToTop(); // плавная прокрутка вверх
|
||
* scrollToTop(false); // мгновенная прокрутка
|
||
*/
|
||
export function scrollToTop(smooth = true) {
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: smooth ? 'smooth' : 'auto'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Прокрутка в конец страницы
|
||
*
|
||
* @param {boolean} smooth - плавная прокрутка (по умолчанию true)
|
||
* @returns {void}
|
||
*
|
||
* @example
|
||
* scrollToBottom(); // плавная прокрутка вниз
|
||
*/
|
||
export function scrollToBottom(smooth = true) {
|
||
window.scrollTo({
|
||
top: document.documentElement.scrollHeight,
|
||
behavior: smooth ? 'smooth' : 'auto'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Проверка видимости элемента в viewport
|
||
*
|
||
* @param {HTMLElement} element - элемент для проверки
|
||
* @param {number} threshold - процент видимости (0-1)
|
||
* @returns {boolean} - виден ли элемент
|
||
*
|
||
* @example
|
||
* if (isElementInViewport(element, 0.5)) {
|
||
* console.log('Элемент виден более чем на 50%');
|
||
* }
|
||
*/
|
||
export function isElementInViewport(element, threshold = 0) {
|
||
const rect = element.getBoundingClientRect();
|
||
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
||
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
||
|
||
const vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
|
||
const horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);
|
||
|
||
if (threshold === 0) {
|
||
return vertInView && horInView;
|
||
}
|
||
|
||
// Процент видимости
|
||
const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);
|
||
const visibleWidth = Math.min(rect.right, windowWidth) - Math.max(rect.left, 0);
|
||
const visibleArea = visibleHeight * visibleWidth;
|
||
const totalArea = rect.height * rect.width;
|
||
|
||
return (visibleArea / totalArea) >= threshold;
|
||
}
|
||
|
||
/**
|
||
* Получение текущей позиции скролла
|
||
*
|
||
* @returns {Object} - объект с координатами { x, y }
|
||
*
|
||
* @example
|
||
* const { x, y } = getScrollPosition();
|
||
* console.log('Scrolled:', x, y);
|
||
*/
|
||
export function getScrollPosition() {
|
||
return {
|
||
x: window.pageXOffset || document.documentElement.scrollLeft,
|
||
y: window.pageYOffset || document.documentElement.scrollTop
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Скрытие/показ элемента при прокрутке (например, кнопки "наверх")
|
||
*
|
||
* @param {HTMLElement} element - элемент для показа/скрытия
|
||
* @param {number} threshold - порог прокрутки в пикселях
|
||
* @param {Function} callback - функция обратного вызова
|
||
* @returns {Function} - функция для отписки от события
|
||
*
|
||
* @example
|
||
* const btn = document.querySelector('#backToTop');
|
||
* const unsubscribe = onScrollThreshold(btn, 300, (visible) => {
|
||
* btn.style.display = visible ? 'block' : 'none';
|
||
* });
|
||
*/
|
||
export function onScrollThreshold(element, threshold, callback) {
|
||
const handleScroll = () => {
|
||
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||
callback(scrollY > threshold);
|
||
};
|
||
|
||
window.addEventListener('scroll', handleScroll);
|
||
handleScroll(); // вызвать сразу
|
||
|
||
// Возвращаем функцию для отписки
|
||
return () => window.removeEventListener('scroll', handleScroll);
|
||
}
|