feat: add infinite scroll to employees list
- Added intersection observer for automatic loading - Track nextUrl and hasMore from API response - Append new employees instead of replacing - Removed old pagination controls from template - Show loading spinner while fetching more data
This commit is contained in:
parent
9ef3c53a46
commit
338b9cf065
@ -15,11 +15,13 @@ export class EmployeesList {
|
||||
this.apiUrl = '/api/v1/employees/';
|
||||
this.employees = [];
|
||||
this.loading = false;
|
||||
this.currentPage = 1;
|
||||
this.hasMore = true;
|
||||
this.nextUrl = null;
|
||||
|
||||
// Параметры из URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.params = {
|
||||
page: urlParams.get('page') || 1,
|
||||
ordering: urlParams.get('o') || 'last_name',
|
||||
search: urlParams.get('q') || '',
|
||||
department: urlParams.get('department') || '',
|
||||
@ -37,6 +39,7 @@ export class EmployeesList {
|
||||
try {
|
||||
await this.loadEmployees();
|
||||
this.render();
|
||||
this.setupInfiniteScroll();
|
||||
} catch (error) {
|
||||
console.error('Failed to load employees:', error);
|
||||
this.renderError();
|
||||
@ -46,11 +49,17 @@ export class EmployeesList {
|
||||
/**
|
||||
* Загрузка сотрудников через API
|
||||
*/
|
||||
async loadEmployees() {
|
||||
if (this.loading) return;
|
||||
async loadEmployees(append = false) {
|
||||
if (this.loading || (!this.hasMore && append)) return;
|
||||
this.loading = true;
|
||||
|
||||
// Показываем loader
|
||||
if (append) {
|
||||
this.showLoadMoreSpinner();
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', this.currentPage);
|
||||
Object.entries(this.params).forEach(([key, value]) => {
|
||||
if (value) params.append(key, value);
|
||||
});
|
||||
@ -61,8 +70,106 @@ export class EmployeesList {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.employees = data.results || data;
|
||||
const newEmployees = data.results || data;
|
||||
|
||||
if (append) {
|
||||
this.employees = [...this.employees, ...newEmployees];
|
||||
} else {
|
||||
this.employees = newEmployees;
|
||||
}
|
||||
|
||||
// Обновляем информацию о пагинации
|
||||
this.nextUrl = data.next;
|
||||
this.hasMore = !!data.next;
|
||||
|
||||
this.loading = false;
|
||||
|
||||
// Убираем loader
|
||||
if (append) {
|
||||
this.hideLoadMoreSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка бесконечной прокрутки
|
||||
*/
|
||||
setupInfiniteScroll() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const lastEntry = entries[0];
|
||||
if (lastEntry.isIntersecting && this.hasMore && !this.loading) {
|
||||
this.loadMore();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '100px',
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
// Создаем маркер для отслеживания
|
||||
const sentinel = document.createElement('div');
|
||||
sentinel.id = 'empListSentinel';
|
||||
sentinel.style.height = '1px';
|
||||
this.container.parentElement.appendChild(sentinel);
|
||||
|
||||
observer.observe(sentinel);
|
||||
this.infiniteScrollObserver = observer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка следующей страницы
|
||||
*/
|
||||
async loadMore() {
|
||||
if (!this.hasMore || this.loading) return;
|
||||
|
||||
this.currentPage++;
|
||||
await this.loadEmployees(true);
|
||||
this.appendNewEmployees();
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление новых сотрудников в конец списка
|
||||
*/
|
||||
appendNewEmployees() {
|
||||
// Находим индекс последнего отрендеренного сотрудника
|
||||
const currentCount = this.container.querySelectorAll('.emp-row').length;
|
||||
const newEmployees = this.employees.slice(currentCount);
|
||||
|
||||
if (newEmployees.length === 0) return;
|
||||
|
||||
const newItemsHtml = newEmployees.map(emp => this.createEmployeeCard(emp)).join('');
|
||||
this.container.insertAdjacentHTML('beforeend', newItemsHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать spinner загрузки
|
||||
*/
|
||||
showLoadMoreSpinner() {
|
||||
let spinner = document.getElementById('empListSpinner');
|
||||
if (!spinner) {
|
||||
spinner = document.createElement('div');
|
||||
spinner.id = 'empListSpinner';
|
||||
spinner.className = 'text-center py-3';
|
||||
spinner.innerHTML = `
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
`;
|
||||
this.container.parentElement.appendChild(spinner);
|
||||
}
|
||||
spinner.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть spinner загрузки
|
||||
*/
|
||||
hideLoadMoreSpinner() {
|
||||
const spinner = document.getElementById('empListSpinner');
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -31,25 +31,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Пагинация #}
|
||||
{% if next_url or prev_url %}
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<a class="btn btn-outline-secondary {% if not prev_url %}disabled{% endif %}"
|
||||
href="{{ prev_url|default:'#' }}"
|
||||
{% if not prev_url %}aria-disabled="true"{% endif %}>
|
||||
<i class="bi-arrow-left"></i> Назад
|
||||
</a>
|
||||
{% if count %}
|
||||
<span class="text-body-secondary small">Всего: {{ count }}</span>
|
||||
{% endif %}
|
||||
<a class="btn btn-outline-secondary {% if not next_url %}disabled{% endif %}"
|
||||
href="{{ next_url|default:'#' }}"
|
||||
{% if not next_url %}aria-disabled="true"{% endif %}>
|
||||
Вперёд <i class="bi-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user