Игорь Надеин c3fd4916d1 refactor: extract JavaScript utilities to separate modules (Phase 2)
- 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 ✅
2025-10-28 08:37:02 +03:00

165 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
}