Игорь Надеин 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

154 lines
5.0 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.

/**
* Timing Utilities
*
* Утилиты для управления временем выполнения функций: debounce, throttle и т.д.
*
* Использование:
* import { debounce, throttle } from '{% static "js/utils/timing.js" %}';
*/
/**
* Debounce - откладывает выполнение функции до окончания "тишины"
* Используется для оптимизации обработчиков событий (поиск, resize, scroll и т.д.)
*
* @param {Function} fn - функция для выполнения
* @param {number} ms - задержка в миллисекундах (по умолчанию 300)
* @returns {Function} - обёрнутая функция
*
* @example
* const handleSearch = debounce((query) => {
* console.log('Searching for:', query);
* }, 300);
*
* // При быстром наборе будет вызван только последний
* handleSearch('a'); // не выполнится
* handleSearch('ab'); // не выполнится
* handleSearch('abc'); // выполнится через 300мс после последнего вызова
*/
export function debounce(fn, ms = 300) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), ms);
};
}
/**
* Throttle - ограничивает частоту выполнения функции
* Гарантирует выполнение не чаще чем раз в указанный интервал
*
* @param {Function} fn - функция для выполнения
* @param {number} ms - минимальный интервал между вызовами (по умолчанию 300)
* @returns {Function} - обёрнутая функция
*
* @example
* const handleScroll = throttle(() => {
* console.log('Scroll position:', window.scrollY);
* }, 100);
*
* // При скролле будет вызываться максимум раз в 100мс
* window.addEventListener('scroll', handleScroll);
*/
export function throttle(fn, ms = 300) {
let inThrottle;
let lastFn;
let lastTime;
return function(...args) {
const context = this;
if (!inThrottle) {
fn.apply(context, args);
lastTime = Date.now();
inThrottle = true;
} else {
clearTimeout(lastFn);
lastFn = setTimeout(() => {
if (Date.now() - lastTime >= ms) {
fn.apply(context, args);
lastTime = Date.now();
}
}, Math.max(ms - (Date.now() - lastTime), 0));
}
};
}
/**
* Delay - создаёт Promise, который разрешается через указанное время
* Полезно для async/await паттернов
*
* @param {number} ms - задержка в миллисекундах
* @returns {Promise} - Promise, который разрешится через указанное время
*
* @example
* async function loadData() {
* console.log('Loading...');
* await delay(1000);
* console.log('Loaded!');
* }
*/
export function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Polling - периодический вызов функции до выполнения условия
*
* @param {Function} fn - функция для выполнения (должна возвращать boolean)
* @param {number} interval - интервал между вызовами в мс
* @param {number} timeout - максимальное время ожидания в мс
* @returns {Promise<boolean>} - Promise, который разрешится когда fn вернёт true
*
* @example
* // Ждём пока элемент появится в DOM
* await poll(
* () => document.querySelector('#myElement') !== null,
* 100, // проверяем каждые 100мс
* 5000 // максимум 5 секунд
* );
*/
export function poll(fn, interval = 100, timeout = 5000) {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const checkCondition = () => {
if (fn()) {
resolve(true);
} else if (Date.now() - startTime >= timeout) {
reject(new Error('Polling timeout'));
} else {
setTimeout(checkCondition, interval);
}
};
checkCondition();
});
}
/**
* Once - гарантирует однократное выполнение функции
*
* @param {Function} fn - функция для выполнения
* @returns {Function} - обёрнутая функция
*
* @example
* const initialize = once(() => {
* console.log('Initialized!');
* });
*
* initialize(); // выведет "Initialized!"
* initialize(); // ничего не произойдёт
*/
export function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}