Added: - Test chat API script - Date group CSS and JS utilities - IntersectionObserver modules - Bidirectional loading test files - Autoscroll testing checklist
255 lines
8.0 KiB
JavaScript
255 lines
8.0 KiB
JavaScript
/**
|
||
* 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;
|