Feat: Add catalog page with inline category renaming
- Create catalog view with category tree and product grid - Add grid/list view toggle for products - Implement inline category name editing (click to rename) - Add API endpoint for category rename - Extract JS to separate catalog.js file - Remove unused partial templates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
228
myproject/products/static/products/js/catalog.js
Normal file
228
myproject/products/static/products/js/catalog.js
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Каталог товаров - JavaScript функционал
|
||||
* - Раскрытие/сворачивание категорий
|
||||
* - Фильтрация и поиск товаров
|
||||
* - Переключение вида (карточки/список)
|
||||
* - Inline-редактирование названий категорий
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// ========================================
|
||||
// Раскрытие/сворачивание категорий
|
||||
// ========================================
|
||||
document.querySelectorAll('.category-header').forEach(header => {
|
||||
header.addEventListener('click', function(e) {
|
||||
// Игнорируем клик по редактируемому полю
|
||||
if (e.target.classList.contains('category-name-editable') ||
|
||||
e.target.classList.contains('category-name-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
const node = this.closest('.category-node');
|
||||
const children = node.querySelector('.category-children');
|
||||
const items = node.querySelector('.category-items');
|
||||
const toggle = this.querySelector('.category-toggle');
|
||||
|
||||
if (children) {
|
||||
children.classList.toggle('d-none');
|
||||
}
|
||||
if (items) {
|
||||
items.classList.toggle('d-none');
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.classList.toggle('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Развернуть все
|
||||
const expandAllBtn = document.getElementById('expand-all');
|
||||
if (expandAllBtn) {
|
||||
expandAllBtn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.category-children, .category-items').forEach(el => el.classList.remove('d-none'));
|
||||
document.querySelectorAll('.category-toggle').forEach(el => el.classList.remove('collapsed'));
|
||||
});
|
||||
}
|
||||
|
||||
// Свернуть все
|
||||
const collapseAllBtn = document.getElementById('collapse-all');
|
||||
if (collapseAllBtn) {
|
||||
collapseAllBtn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.category-children, .category-items').forEach(el => el.classList.add('d-none'));
|
||||
document.querySelectorAll('.category-toggle').forEach(el => el.classList.add('collapsed'));
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Фильтр по типу (товары/комплекты)
|
||||
// ========================================
|
||||
document.querySelectorAll('[data-filter]').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('[data-filter]').forEach(b => {
|
||||
b.classList.remove('active', 'btn-primary');
|
||||
b.classList.add('btn-outline-primary');
|
||||
});
|
||||
this.classList.add('active', 'btn-primary');
|
||||
this.classList.remove('btn-outline-primary');
|
||||
|
||||
const filter = this.dataset.filter;
|
||||
document.querySelectorAll('.catalog-item').forEach(item => {
|
||||
item.style.display = (filter === 'all' || item.dataset.type === filter) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Поиск
|
||||
// ========================================
|
||||
const searchInput = document.getElementById('catalog-search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.toLowerCase();
|
||||
document.querySelectorAll('.catalog-item').forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(query) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Переключение вида: карточки / список
|
||||
// ========================================
|
||||
const catalogGrid = document.getElementById('catalog-grid');
|
||||
const viewGridBtn = document.getElementById('view-grid');
|
||||
const viewListBtn = document.getElementById('view-list');
|
||||
|
||||
if (viewGridBtn && viewListBtn && catalogGrid) {
|
||||
viewGridBtn.addEventListener('click', function() {
|
||||
catalogGrid.classList.remove('catalog-list');
|
||||
viewGridBtn.classList.add('active');
|
||||
viewListBtn.classList.remove('active');
|
||||
// Восстановить колонки для карточек
|
||||
document.querySelectorAll('.catalog-item').forEach(item => {
|
||||
item.className = item.className.replace(/col-\d+|col-\w+-\d+/g, '');
|
||||
item.classList.add('col-6', 'col-lg-4', 'col-xl-3', 'catalog-item');
|
||||
});
|
||||
});
|
||||
|
||||
viewListBtn.addEventListener('click', function() {
|
||||
catalogGrid.classList.add('catalog-list');
|
||||
viewListBtn.classList.add('active');
|
||||
viewGridBtn.classList.remove('active');
|
||||
// Убрать колонки для списка
|
||||
document.querySelectorAll('.catalog-item').forEach(item => {
|
||||
item.className = item.className.replace(/col-\d+|col-\w+-\d+/g, '');
|
||||
item.classList.add('col-12', 'catalog-item');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Inline-редактирование названий категорий
|
||||
// ========================================
|
||||
document.querySelectorAll('.category-name-editable').forEach(nameSpan => {
|
||||
nameSpan.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
// Уже редактируется?
|
||||
if (this.querySelector('input')) return;
|
||||
|
||||
const categoryId = this.dataset.categoryId;
|
||||
const currentName = this.textContent.trim();
|
||||
|
||||
// Создаем input
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = currentName;
|
||||
input.className = 'category-name-input';
|
||||
input.style.cssText = 'width: 100%; border: 1px solid #0d6efd; border-radius: 3px; padding: 2px 6px; font-size: inherit; outline: none;';
|
||||
|
||||
// Сохраняем оригинальный текст
|
||||
this.dataset.originalName = currentName;
|
||||
this.textContent = '';
|
||||
this.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
// Функция сохранения
|
||||
const saveCategory = async () => {
|
||||
const newName = input.value.trim();
|
||||
|
||||
if (!newName) {
|
||||
// Отмена - пустое название
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newName === this.dataset.originalName) {
|
||||
// Название не изменилось
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем спиннер
|
||||
input.disabled = true;
|
||||
input.style.opacity = '0.5';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/products/api/categories/${categoryId}/rename/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({ name: newName })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Успешно сохранено
|
||||
this.textContent = data.name;
|
||||
} else {
|
||||
// Ошибка
|
||||
alert(data.error || 'Ошибка при сохранении');
|
||||
cancelEdit();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка сети');
|
||||
cancelEdit();
|
||||
}
|
||||
};
|
||||
|
||||
// Функция отмены
|
||||
const cancelEdit = () => {
|
||||
this.textContent = this.dataset.originalName;
|
||||
};
|
||||
|
||||
// Enter - сохранить
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
saveCategory();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelEdit();
|
||||
}
|
||||
});
|
||||
|
||||
// Потеря фокуса - сохранить
|
||||
input.addEventListener('blur', function() {
|
||||
// Небольшая задержка чтобы не конфликтовать с Enter
|
||||
setTimeout(saveCategory, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Получение CSRF токена
|
||||
function getCsrfToken() {
|
||||
const cookieValue = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('csrftoken='))
|
||||
?.split('=')[1];
|
||||
return cookieValue || '';
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user