Corp/backend/static/js/observers/stickyDateObserver.js
Игорь Надеин 0e2a08fb38 chore: Add remaining test files and utilities
Added:
- Test chat API script
- Date group CSS and JS utilities
- IntersectionObserver modules
- Bidirectional loading test files
- Autoscroll testing checklist
2026-01-13 15:02:29 +03:00

255 lines
8.0 KiB
JavaScript
Raw 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.

/**
* StickyDateObserver - IntersectionObserver для отслеживания sticky состояния дат
*
* Основано на Telegram Web (StickyIntersector):
* - Отслеживает когда date header "прилипает" к верху
* - Добавляет/удаляет класс .is-sticky
* - Управляет видимостью оригинальной даты
*
* @module observers/stickyDateObserver
*/
/**
* Observer для отслеживания sticky состояния date headers
*/
export class StickyDateObserver {
/**
* @param {Object} options - Опции конфигурации
* @param {HTMLElement} options.container - Scroll контейнер для наблюдения
* @param {Function} [options.onSticky] - Callback при изменении sticky состояния
*/
constructor(options = {}) {
this.container = options.container;
this.onSticky = options.onSticky || null;
// IntersectionObserver для отслеживания
this.observer = null;
// Set наблюдаемых элементов
this.observedDates = new Set();
console.log('[StickyDateObserver] Initialized');
}
/**
* Инициализирует IntersectionObserver
*
* @example
* observer.init();
*/
init() {
if (this.observer) {
console.warn('[StickyDateObserver] Already initialized');
return;
}
// Создаем observer с нужными настройками
this.observer = new IntersectionObserver(
(entries) => this._handleIntersection(entries),
{
root: this.container,
threshold: [0, 1], // Отслеживаем полное и частичное пересечение
rootMargin: '-56px 0px 0px 0px' // Offset на высоту header
}
);
console.log('[StickyDateObserver] IntersectionObserver created');
// Наблюдаем за всеми существующими date headers
this.observeAll();
}
/**
* Обработчик IntersectionObserver
* @private
*/
_handleIntersection(entries) {
for (const entry of entries) {
const stickyDate = entry.target.parentElement;
if (!stickyDate || !stickyDate.classList.contains('sticky-date')) {
continue;
}
const targetInfo = entry.boundingClientRect;
const rootBoundsInfo = entry.rootBounds;
if (!rootBoundsInfo) continue;
// Определяем sticky состояние
// Элемент "прилип" если его верх выше root bounds top
const isSticky = targetInfo.bottom < rootBoundsInfo.top;
// Обновляем класс
if (isSticky) {
if (!stickyDate.classList.contains('is-sticky')) {
stickyDate.classList.add('is-sticky');
this._notifySticky(stickyDate, true);
}
} else {
if (stickyDate.classList.contains('is-sticky')) {
stickyDate.classList.remove('is-sticky');
this._notifySticky(stickyDate, false);
}
}
}
}
/**
* Уведомляет о изменении sticky состояния
* @private
*/
_notifySticky(stickyDate, isSticky) {
const dateText = stickyDate.getAttribute('data-date-text') ||
stickyDate.querySelector('span')?.textContent;
console.log('[StickyDateObserver] Sticky state changed:', {
date: dateText,
isSticky
});
if (this.onSticky) {
this.onSticky(isSticky, stickyDate, dateText);
}
}
/**
* Начинает наблюдение за всеми date headers в контейнере
*
* @example
* observer.observeAll();
*/
observeAll() {
if (!this.observer) {
console.warn('[StickyDateObserver] Observer not initialized');
return;
}
const stickyDates = this.container.querySelectorAll('.sticky-date');
for (const stickyDate of stickyDates) {
// Для IntersectionObserver нам нужен элемент-"сентинель"
// Telegram Web использует вложенный div, мы будем наблюдать за span
const sentinel = stickyDate.querySelector('span');
if (sentinel && !this.observedDates.has(sentinel)) {
this.observer.observe(sentinel);
this.observedDates.add(sentinel);
}
}
console.log('[StickyDateObserver] Observing dates:', this.observedDates.size);
}
/**
* Начинает наблюдение за конкретным date header
*
* @param {HTMLElement} stickyDateEl - Элемент .sticky-date
*
* @example
* const dateGroup = document.querySelector('.message-date-group');
* const stickyDate = dateGroup.querySelector('.sticky-date');
* observer.observe(stickyDate);
*/
observe(stickyDateEl) {
if (!this.observer) {
console.warn('[StickyDateObserver] Observer not initialized');
return;
}
const sentinel = stickyDateEl.querySelector('span');
if (sentinel && !this.observedDates.has(sentinel)) {
this.observer.observe(sentinel);
this.observedDates.add(sentinel);
console.log('[StickyDateObserver] Started observing date');
}
}
/**
* Прекращает наблюдение за конкретным date header
*
* @param {HTMLElement} stickyDateEl - Элемент .sticky-date
*
* @example
* observer.unobserve(stickyDate);
*/
unobserve(stickyDateEl) {
if (!this.observer) return;
const sentinel = stickyDateEl.querySelector('span');
if (sentinel && this.observedDates.has(sentinel)) {
this.observer.unobserve(sentinel);
this.observedDates.delete(sentinel);
// Удаляем sticky класс
stickyDateEl.classList.remove('is-sticky');
console.log('[StickyDateObserver] Stopped observing date');
}
}
/**
* Обновляет список наблюдаемых элементов
* Полезно после добавления/удаления date groups
*
* @example
* // После загрузки новых сообщений
* observer.refresh();
*/
refresh() {
if (!this.observer) return;
// Прекращаем наблюдение за всеми
for (const sentinel of this.observedDates) {
this.observer.unobserve(sentinel);
}
this.observedDates.clear();
// Начинаем заново
this.observeAll();
console.log('[StickyDateObserver] Refreshed');
}
/**
* Уничтожает observer и очищает ресурсы
*
* @example
* // При unmount компонента
* observer.destroy();
*/
destroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
this.observedDates.clear();
console.log('[StickyDateObserver] Destroyed');
}
/**
* Проверяет, инициализирован ли observer
*
* @returns {boolean}
*/
isInitialized() {
return this.observer !== null;
}
/**
* Получает количество наблюдаемых элементов
*
* @returns {number}
*/
getObservedCount() {
return this.observedDates.size;
}
}
export default StickyDateObserver;