Создан переиспользуемый компонент поиска товаров
- product_search_picker.html - универсальный шаблон компонента поиска и выбора товаров - product-search-picker.js - JavaScript модуль с поддержкой фильтрации по категориям, тегам, наличию - product-search-picker.css - стили для компонента - Поддержка одиночного и множественного выбора товаров - Фильтрация по категориям, тегам и наличию на складе - Отображение фото товара в результатах поиска - Адаптивный интерфейс с прокруткой для больших списков - API для программного управления (init, search, clearSelection и др.) - Возможность кастомизации через параметры (заголовок, высота, текст кнопок)
This commit is contained in:
170
myproject/products/static/products/css/product-search-picker.css
Normal file
170
myproject/products/static/products/css/product-search-picker.css
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* ProductSearchPicker - стили компонента поиска и выбора товаров
|
||||
*/
|
||||
|
||||
.product-search-picker .product-picker-item {
|
||||
transition: all 0.15s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-item:hover {
|
||||
border-color: #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-item.selected {
|
||||
border-color: #198754 !important;
|
||||
background-color: #d1e7dd !important;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-item.selected .card {
|
||||
border-color: #198754;
|
||||
}
|
||||
|
||||
/* Grid view - карточки */
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item .card {
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item:hover .card {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item.selected .card {
|
||||
box-shadow: 0 0 0 2px #198754;
|
||||
}
|
||||
|
||||
/* List view - список */
|
||||
.product-search-picker .product-picker-grid[data-view="list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-grid[data-view="list"] .col-6,
|
||||
.product-search-picker .product-picker-grid[data-view="list"] .col-lg-4,
|
||||
.product-search-picker .product-picker-grid[data-view="list"] .col-xl-3 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.product-search-picker .product-picker-loading {
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Контент - scrollable */
|
||||
.product-search-picker .product-picker-content {
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* Фильтры */
|
||||
.product-search-picker .product-picker-filters {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Счетчик выбранных */
|
||||
.product-search-picker .product-picker-selected-count {
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Иконка выбора в list view */
|
||||
.product-search-picker .product-picker-grid[data-view="list"] .bi-circle,
|
||||
.product-search-picker .product-picker-grid[data-view="list"] .bi-check-circle-fill {
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Иконка выбора в grid view */
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] .check-icon {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Пустой результат */
|
||||
.product-search-picker .product-picker-empty {
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Кнопка очистки поиска */
|
||||
.product-search-picker .product-picker-search-clear {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
/* Hover эффект для кнопок действий */
|
||||
.product-search-picker .card-footer .btn {
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] .col-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.product-search-picker .card-header .d-flex {
|
||||
flex-direction: column;
|
||||
align-items: stretch !important;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.product-search-picker .card-header .input-group {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.product-search-picker .card-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.product-search-picker .card-footer > div {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.product-search-picker .card-footer .btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.product-search-picker .product-picker-grid[data-view="grid"] .col-6 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-filters .row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-filters .col-auto {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-search-picker .product-picker-filters .col-auto.ms-auto {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
747
myproject/products/static/products/js/product-search-picker.js
Normal file
747
myproject/products/static/products/js/product-search-picker.js
Normal file
@@ -0,0 +1,747 @@
|
||||
/**
|
||||
* ProductSearchPicker - переиспользуемый модуль для поиска и выбора товаров
|
||||
*
|
||||
* API:
|
||||
* - ProductSearchPicker.init(containerSelector, options) - инициализация
|
||||
* - instance.getSelected() - получить выбранные товары (массив объектов)
|
||||
* - instance.getSelectedIds() - получить ID выбранных товаров (массив строк)
|
||||
* - instance.clearSelection() - сбросить выбор
|
||||
* - instance.setSelection(productIds) - установить выбор программно
|
||||
* - instance.refresh() - обновить список товаров
|
||||
* - instance.destroy() - уничтожить экземпляр
|
||||
*
|
||||
* Пример использования:
|
||||
* const picker = ProductSearchPicker.init('#my-picker', {
|
||||
* onAddSelected: function(products, instance) {
|
||||
* console.log('Добавлены товары:', products);
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
// Приватное хранилище экземпляров
|
||||
var instances = {};
|
||||
|
||||
/**
|
||||
* Конструктор компонента
|
||||
*/
|
||||
function ProductSearchPicker(container, options) {
|
||||
this.container = typeof container === 'string'
|
||||
? document.querySelector(container)
|
||||
: container;
|
||||
|
||||
if (!this.container) {
|
||||
console.error('ProductSearchPicker: container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Настройки по умолчанию
|
||||
this.options = Object.assign({
|
||||
apiUrl: this.container.dataset.apiUrl || '/products/api/search-products-variants/',
|
||||
multiSelect: this.container.dataset.multiSelect !== 'false',
|
||||
maxSelection: parseInt(this.container.dataset.maxSelection) || null,
|
||||
excludeKits: this.container.dataset.excludeKits !== 'false',
|
||||
debounceDelay: 300,
|
||||
pageSize: 30,
|
||||
|
||||
// Callbacks
|
||||
onSelect: null, // (products, instance) => {}
|
||||
onDeselect: null, // (products, instance) => {}
|
||||
onSelectionChange: null, // (selectedProducts, instance) => {}
|
||||
onAddSelected: null // (selectedProducts, instance) => {}
|
||||
}, options || {});
|
||||
|
||||
// Состояние
|
||||
this.state = {
|
||||
products: [],
|
||||
selected: {}, // {productId: productData}
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
isLoading: false,
|
||||
currentView: 'list',
|
||||
filters: {
|
||||
search: '',
|
||||
category: '',
|
||||
tag: '',
|
||||
inStock: false
|
||||
}
|
||||
};
|
||||
|
||||
// Кэшируем DOM элементы
|
||||
this._cacheElements();
|
||||
|
||||
// Привязываем обработчики
|
||||
this._bindEvents();
|
||||
|
||||
// Начальная загрузка
|
||||
this._loadProducts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Кэширование DOM элементов
|
||||
*/
|
||||
ProductSearchPicker.prototype._cacheElements = function() {
|
||||
var c = this.container;
|
||||
this.elements = {
|
||||
searchInput: c.querySelector('.product-picker-search'),
|
||||
searchClear: c.querySelector('.product-picker-search-clear'),
|
||||
categorySelect: c.querySelector('.product-picker-category'),
|
||||
tagSelect: c.querySelector('.product-picker-tag'),
|
||||
inStockCheckbox: c.querySelector('.product-picker-in-stock'),
|
||||
grid: c.querySelector('.product-picker-grid'),
|
||||
loading: c.querySelector('.product-picker-loading'),
|
||||
empty: c.querySelector('.product-picker-empty'),
|
||||
content: c.querySelector('.product-picker-content'),
|
||||
selectedCount: c.querySelector('.product-picker-selected-count'),
|
||||
selectAllBtn: c.querySelector('.product-picker-select-all'),
|
||||
clearSelectionBtn: c.querySelector('.product-picker-clear-selection'),
|
||||
addSelectedBtn: c.querySelector('.product-picker-add-selected'),
|
||||
viewButtons: c.querySelectorAll('.product-picker-view')
|
||||
};
|
||||
|
||||
// Начальный вид из data-атрибута
|
||||
if (this.elements.grid) {
|
||||
var initialView = this.elements.grid.dataset.view || 'list';
|
||||
this.state.currentView = initialView;
|
||||
this._updateViewButtons();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Привязка обработчиков событий
|
||||
*/
|
||||
ProductSearchPicker.prototype._bindEvents = function() {
|
||||
var self = this;
|
||||
var debounceTimer;
|
||||
|
||||
// Поиск с debounce
|
||||
if (this.elements.searchInput) {
|
||||
this.elements.searchInput.addEventListener('input', function() {
|
||||
clearTimeout(debounceTimer);
|
||||
var value = this.value.trim();
|
||||
|
||||
// Показать/скрыть кнопку очистки
|
||||
if (self.elements.searchClear) {
|
||||
self.elements.searchClear.style.display = value ? 'block' : 'none';
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(function() {
|
||||
self.state.filters.search = value;
|
||||
self.state.currentPage = 1;
|
||||
self._loadProducts();
|
||||
}, self.options.debounceDelay);
|
||||
});
|
||||
|
||||
// Поиск по Enter
|
||||
this.elements.searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
clearTimeout(debounceTimer);
|
||||
self.state.filters.search = this.value.trim();
|
||||
self.state.currentPage = 1;
|
||||
self._loadProducts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка очистки поиска
|
||||
if (this.elements.searchClear) {
|
||||
this.elements.searchClear.addEventListener('click', function() {
|
||||
self.elements.searchInput.value = '';
|
||||
this.style.display = 'none';
|
||||
self.state.filters.search = '';
|
||||
self.state.currentPage = 1;
|
||||
self._loadProducts();
|
||||
});
|
||||
}
|
||||
|
||||
// Фильтр по категории
|
||||
if (this.elements.categorySelect) {
|
||||
this.elements.categorySelect.addEventListener('change', function() {
|
||||
self.state.filters.category = this.value;
|
||||
self.state.currentPage = 1;
|
||||
self._loadProducts();
|
||||
});
|
||||
}
|
||||
|
||||
// Фильтр по тегу
|
||||
if (this.elements.tagSelect) {
|
||||
this.elements.tagSelect.addEventListener('change', function() {
|
||||
self.state.filters.tag = this.value;
|
||||
self.state.currentPage = 1;
|
||||
self._loadProducts();
|
||||
});
|
||||
}
|
||||
|
||||
// Фильтр по наличию
|
||||
if (this.elements.inStockCheckbox) {
|
||||
this.elements.inStockCheckbox.addEventListener('change', function() {
|
||||
self.state.filters.inStock = this.checked;
|
||||
self.state.currentPage = 1;
|
||||
self._loadProducts();
|
||||
});
|
||||
// Инициализация из checkbox
|
||||
this.state.filters.inStock = this.elements.inStockCheckbox.checked;
|
||||
}
|
||||
|
||||
// Переключатель вида
|
||||
this.elements.viewButtons.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var view = this.dataset.view;
|
||||
self._setView(view);
|
||||
});
|
||||
});
|
||||
|
||||
// Клик по товару (делегирование)
|
||||
if (this.elements.grid) {
|
||||
this.elements.grid.addEventListener('click', function(e) {
|
||||
var productCard = e.target.closest('.product-picker-item');
|
||||
if (productCard) {
|
||||
var productId = productCard.dataset.productId;
|
||||
self._toggleProduct(productId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Выбрать все
|
||||
if (this.elements.selectAllBtn) {
|
||||
this.elements.selectAllBtn.addEventListener('click', function() {
|
||||
self._selectAll();
|
||||
});
|
||||
}
|
||||
|
||||
// Сбросить выбор
|
||||
if (this.elements.clearSelectionBtn) {
|
||||
this.elements.clearSelectionBtn.addEventListener('click', function() {
|
||||
self.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
// Добавить выбранные
|
||||
if (this.elements.addSelectedBtn) {
|
||||
this.elements.addSelectedBtn.addEventListener('click', function() {
|
||||
if (self.options.onAddSelected) {
|
||||
self.options.onAddSelected(self.getSelected(), self);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Infinite scroll
|
||||
if (this.elements.content) {
|
||||
this.elements.content.addEventListener('scroll', function() {
|
||||
if (self.state.hasMore && !self.state.isLoading) {
|
||||
var scrollTop = this.scrollTop;
|
||||
var scrollHeight = this.scrollHeight;
|
||||
var clientHeight = this.clientHeight;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||
self.state.currentPage++;
|
||||
self._loadProducts(true); // append mode
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Загрузка товаров через AJAX
|
||||
*/
|
||||
ProductSearchPicker.prototype._loadProducts = function(append) {
|
||||
var self = this;
|
||||
|
||||
if (this.state.isLoading) return;
|
||||
this.state.isLoading = true;
|
||||
|
||||
// Показать индикатор загрузки
|
||||
if (!append) {
|
||||
if (this.elements.loading) this.elements.loading.style.display = 'flex';
|
||||
if (this.elements.grid) this.elements.grid.innerHTML = '';
|
||||
if (this.elements.empty) this.elements.empty.style.display = 'none';
|
||||
}
|
||||
|
||||
// Формируем параметры запроса
|
||||
var params = new URLSearchParams({
|
||||
type: 'product', // Только штучные товары
|
||||
page: this.state.currentPage
|
||||
});
|
||||
|
||||
if (this.state.filters.search) {
|
||||
params.append('q', this.state.filters.search);
|
||||
}
|
||||
if (this.state.filters.category) {
|
||||
params.append('category', this.state.filters.category);
|
||||
}
|
||||
if (this.state.filters.tag) {
|
||||
params.append('tag', this.state.filters.tag);
|
||||
}
|
||||
if (this.state.filters.inStock) {
|
||||
params.append('in_stock', 'true');
|
||||
}
|
||||
|
||||
fetch(this.options.apiUrl + '?' + params.toString())
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
self.state.isLoading = false;
|
||||
if (self.elements.loading) self.elements.loading.style.display = 'none';
|
||||
|
||||
// Извлекаем товары (учитываем группировку в API)
|
||||
var products = [];
|
||||
if (Array.isArray(data.results)) {
|
||||
data.results.forEach(function(item) {
|
||||
if (item.children) {
|
||||
// Группированные результаты
|
||||
item.children.forEach(function(p) {
|
||||
if (p.type === 'product') {
|
||||
products.push(p);
|
||||
}
|
||||
});
|
||||
} else if (item.type === 'product') {
|
||||
products.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (append) {
|
||||
self.state.products = self.state.products.concat(products);
|
||||
} else {
|
||||
self.state.products = products;
|
||||
}
|
||||
|
||||
self.state.hasMore = data.pagination && data.pagination.more;
|
||||
|
||||
// Рендеринг
|
||||
self._renderProducts(products, append);
|
||||
|
||||
// Показать "пусто" если нет результатов
|
||||
if (self.state.products.length === 0) {
|
||||
if (self.elements.empty) self.elements.empty.style.display = 'flex';
|
||||
} else {
|
||||
if (self.elements.empty) self.elements.empty.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('ProductSearchPicker: fetch error', error);
|
||||
self.state.isLoading = false;
|
||||
if (self.elements.loading) self.elements.loading.style.display = 'none';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Рендеринг товаров
|
||||
*/
|
||||
ProductSearchPicker.prototype._renderProducts = function(products, append) {
|
||||
var self = this;
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
products.forEach(function(product) {
|
||||
// Извлекаем ID (убираем префикс "product_")
|
||||
var productId = String(product.id).replace('product_', '');
|
||||
var isSelected = self.state.selected.hasOwnProperty(productId);
|
||||
|
||||
var html = self.state.currentView === 'grid'
|
||||
? self._renderGridCard(product, productId, isSelected)
|
||||
: self._renderListItem(product, productId, isSelected);
|
||||
|
||||
var temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
fragment.appendChild(temp.firstElementChild);
|
||||
});
|
||||
|
||||
if (append) {
|
||||
this.elements.grid.appendChild(fragment);
|
||||
} else {
|
||||
this.elements.grid.innerHTML = '';
|
||||
this.elements.grid.appendChild(fragment);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Рендеринг карточки товара (grid view)
|
||||
*/
|
||||
ProductSearchPicker.prototype._renderGridCard = function(product, productId, isSelected) {
|
||||
var inStockBadge = product.in_stock
|
||||
? '<span class="badge bg-success" style="font-size: 0.65rem;">В наличии</span>'
|
||||
: '<span class="badge bg-secondary" style="font-size: 0.65rem;">Нет</span>';
|
||||
|
||||
var selectedClass = isSelected ? 'selected' : '';
|
||||
var checkIcon = isSelected
|
||||
? '<i class="bi bi-check-circle-fill text-success check-icon"></i>'
|
||||
: '';
|
||||
|
||||
var photoHtml = product.photo_url
|
||||
? '<img src="' + product.photo_url + '" class="card-img-top" style="height: 100px; object-fit: cover;" alt="' + (product.text || '') + '">'
|
||||
: '<div class="bg-light d-flex align-items-center justify-content-center" style="height: 100px;"><i class="bi bi-image text-muted fs-2"></i></div>';
|
||||
|
||||
var name = product.text || product.name || '';
|
||||
var price = product.actual_price || product.price || '0';
|
||||
|
||||
return '<div class="col-6 col-lg-4 col-xl-3">' +
|
||||
'<div class="card product-picker-item h-100 shadow-sm ' + selectedClass + '" ' +
|
||||
'data-product-id="' + productId + '" style="cursor: pointer;">' +
|
||||
'<div class="position-relative">' +
|
||||
photoHtml +
|
||||
checkIcon +
|
||||
'</div>' +
|
||||
'<div class="card-body p-2">' +
|
||||
'<div class="small text-truncate fw-medium">' + name + '</div>' +
|
||||
'<div class="d-flex justify-content-between align-items-center mt-1">' +
|
||||
'<span class="fw-bold text-primary small">' + price + ' руб.</span>' +
|
||||
inStockBadge +
|
||||
'</div>' +
|
||||
'<small class="text-muted">' + (product.sku || '') + '</small>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Рендеринг строки товара (list view)
|
||||
*/
|
||||
ProductSearchPicker.prototype._renderListItem = function(product, productId, isSelected) {
|
||||
var inStockBadge = product.in_stock
|
||||
? '<span class="badge bg-success">В наличии</span>'
|
||||
: '<span class="badge bg-secondary">Нет</span>';
|
||||
|
||||
var selectedClass = isSelected ? 'selected' : '';
|
||||
var checkIcon = isSelected
|
||||
? '<i class="bi bi-check-circle-fill text-success me-2"></i>'
|
||||
: '<i class="bi bi-circle text-muted me-2"></i>';
|
||||
|
||||
var photoHtml = product.photo_url
|
||||
? '<img src="' + product.photo_url + '" class="rounded" style="width: 40px; height: 40px; object-fit: cover;">'
|
||||
: '<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-image text-muted"></i></div>';
|
||||
|
||||
var name = product.text || product.name || '';
|
||||
var price = product.actual_price || product.price || '0';
|
||||
|
||||
return '<div class="col-12">' +
|
||||
'<div class="product-picker-item d-flex align-items-center p-2 border rounded ' + selectedClass + '" ' +
|
||||
'data-product-id="' + productId + '" style="cursor: pointer;">' +
|
||||
checkIcon +
|
||||
'<div class="flex-shrink-0 me-2">' +
|
||||
photoHtml +
|
||||
'</div>' +
|
||||
'<div class="flex-grow-1" style="min-width: 0;">' +
|
||||
'<div class="text-truncate fw-medium">' + name + '</div>' +
|
||||
'<small class="text-muted">' + (product.sku || '') + '</small>' +
|
||||
'</div>' +
|
||||
'<div class="text-end ms-2 text-nowrap">' +
|
||||
'<div class="fw-bold text-primary">' + price + ' руб.</div>' +
|
||||
inStockBadge +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Переключение выбора товара
|
||||
*/
|
||||
ProductSearchPicker.prototype._toggleProduct = function(productId) {
|
||||
var self = this;
|
||||
var product = null;
|
||||
|
||||
// Находим товар в списке
|
||||
for (var i = 0; i < this.state.products.length; i++) {
|
||||
var p = this.state.products[i];
|
||||
if (String(p.id).replace('product_', '') === productId) {
|
||||
product = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!product) return;
|
||||
|
||||
var isSelected = this.state.selected.hasOwnProperty(productId);
|
||||
|
||||
if (isSelected) {
|
||||
// Снять выбор
|
||||
delete this.state.selected[productId];
|
||||
if (this.options.onDeselect) {
|
||||
this.options.onDeselect([product], this);
|
||||
}
|
||||
} else {
|
||||
// Проверка множественного выбора
|
||||
if (!this.options.multiSelect) {
|
||||
this.state.selected = {};
|
||||
}
|
||||
|
||||
// Проверка лимита
|
||||
if (this.options.maxSelection &&
|
||||
Object.keys(this.state.selected).length >= this.options.maxSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавить в выбор
|
||||
this.state.selected[productId] = product;
|
||||
if (this.options.onSelect) {
|
||||
this.options.onSelect([product], this);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить UI
|
||||
this._updateProductUI(productId);
|
||||
this._updateSelectionUI();
|
||||
|
||||
// Общий callback
|
||||
if (this.options.onSelectionChange) {
|
||||
this.options.onSelectionChange(this.getSelected(), this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Обновление UI товара
|
||||
*/
|
||||
ProductSearchPicker.prototype._updateProductUI = function(productId) {
|
||||
var card = this.elements.grid.querySelector('[data-product-id="' + productId + '"]');
|
||||
if (!card) return;
|
||||
|
||||
var isSelected = this.state.selected.hasOwnProperty(productId);
|
||||
|
||||
if (isSelected) {
|
||||
card.classList.add('selected');
|
||||
// Обновить иконку
|
||||
var icon = card.querySelector('.bi-circle');
|
||||
if (icon) {
|
||||
icon.classList.remove('bi-circle', 'text-muted');
|
||||
icon.classList.add('bi-check-circle-fill', 'text-success');
|
||||
}
|
||||
// Добавить иконку в grid view если её нет
|
||||
if (this.state.currentView === 'grid') {
|
||||
var posRel = card.querySelector('.position-relative');
|
||||
if (posRel && !posRel.querySelector('.check-icon')) {
|
||||
var checkEl = document.createElement('i');
|
||||
checkEl.className = 'bi bi-check-circle-fill text-success check-icon';
|
||||
posRel.appendChild(checkEl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
var iconCheck = card.querySelector('.bi-check-circle-fill');
|
||||
if (iconCheck) {
|
||||
// В list view меняем иконку
|
||||
if (this.state.currentView === 'list') {
|
||||
iconCheck.classList.remove('bi-check-circle-fill', 'text-success');
|
||||
iconCheck.classList.add('bi-circle', 'text-muted');
|
||||
} else {
|
||||
// В grid view удаляем иконку
|
||||
if (iconCheck.classList.contains('check-icon')) {
|
||||
iconCheck.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Обновление UI выбора (счетчик, кнопки)
|
||||
*/
|
||||
ProductSearchPicker.prototype._updateSelectionUI = function() {
|
||||
var count = Object.keys(this.state.selected).length;
|
||||
|
||||
// Счетчик
|
||||
if (this.elements.selectedCount) {
|
||||
this.elements.selectedCount.textContent = count;
|
||||
}
|
||||
|
||||
// Кнопка "Добавить"
|
||||
if (this.elements.addSelectedBtn) {
|
||||
this.elements.addSelectedBtn.disabled = count === 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Выбрать все видимые товары
|
||||
*/
|
||||
ProductSearchPicker.prototype._selectAll = function() {
|
||||
var self = this;
|
||||
var currentCount = Object.keys(this.state.selected).length;
|
||||
var maxToSelect = this.options.maxSelection
|
||||
? this.options.maxSelection - currentCount
|
||||
: Infinity;
|
||||
|
||||
var added = 0;
|
||||
this.state.products.forEach(function(product) {
|
||||
if (added >= maxToSelect) return;
|
||||
|
||||
var productId = String(product.id).replace('product_', '');
|
||||
if (!self.state.selected.hasOwnProperty(productId)) {
|
||||
self.state.selected[productId] = product;
|
||||
self._updateProductUI(productId);
|
||||
added++;
|
||||
}
|
||||
});
|
||||
|
||||
this._updateSelectionUI();
|
||||
|
||||
if (this.options.onSelectionChange) {
|
||||
this.options.onSelectionChange(this.getSelected(), this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Обновление кнопок переключателя вида
|
||||
*/
|
||||
ProductSearchPicker.prototype._updateViewButtons = function() {
|
||||
var self = this;
|
||||
this.elements.viewButtons.forEach(function(btn) {
|
||||
if (btn.dataset.view === self.state.currentView) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Переключение вида (grid/list)
|
||||
*/
|
||||
ProductSearchPicker.prototype._setView = function(view) {
|
||||
this.state.currentView = view;
|
||||
|
||||
// Обновить data-атрибут
|
||||
if (this.elements.grid) {
|
||||
this.elements.grid.dataset.view = view;
|
||||
}
|
||||
|
||||
// Обновить кнопки
|
||||
this._updateViewButtons();
|
||||
|
||||
// Перерисовать
|
||||
this._renderProducts(this.state.products, false);
|
||||
};
|
||||
|
||||
// ========== ПУБЛИЧНОЕ API ==========
|
||||
|
||||
/**
|
||||
* Получить массив выбранных товаров
|
||||
*/
|
||||
ProductSearchPicker.prototype.getSelected = function() {
|
||||
var result = [];
|
||||
for (var key in this.state.selected) {
|
||||
if (this.state.selected.hasOwnProperty(key)) {
|
||||
result.push(this.state.selected[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить массив ID выбранных товаров
|
||||
*/
|
||||
ProductSearchPicker.prototype.getSelectedIds = function() {
|
||||
return Object.keys(this.state.selected);
|
||||
};
|
||||
|
||||
/**
|
||||
* Сбросить выбор
|
||||
*/
|
||||
ProductSearchPicker.prototype.clearSelection = function() {
|
||||
var self = this;
|
||||
var ids = Object.keys(this.state.selected);
|
||||
this.state.selected = {};
|
||||
|
||||
ids.forEach(function(productId) {
|
||||
self._updateProductUI(productId);
|
||||
});
|
||||
|
||||
this._updateSelectionUI();
|
||||
|
||||
if (this.options.onSelectionChange) {
|
||||
this.options.onSelectionChange([], this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Установить выбор программно
|
||||
*/
|
||||
ProductSearchPicker.prototype.setSelection = function(productIds) {
|
||||
var self = this;
|
||||
this.clearSelection();
|
||||
|
||||
productIds.forEach(function(id) {
|
||||
var productId = String(id).replace('product_', '');
|
||||
// Ищем товар в загруженном списке
|
||||
for (var i = 0; i < self.state.products.length; i++) {
|
||||
var p = self.state.products[i];
|
||||
if (String(p.id).replace('product_', '') === productId) {
|
||||
self.state.selected[productId] = p;
|
||||
self._updateProductUI(productId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._updateSelectionUI();
|
||||
};
|
||||
|
||||
/**
|
||||
* Обновить список товаров
|
||||
*/
|
||||
ProductSearchPicker.prototype.refresh = function() {
|
||||
this.state.currentPage = 1;
|
||||
this._loadProducts();
|
||||
};
|
||||
|
||||
/**
|
||||
* Уничтожить экземпляр
|
||||
*/
|
||||
ProductSearchPicker.prototype.destroy = function() {
|
||||
// Очистить состояние
|
||||
this.state.products = [];
|
||||
this.state.selected = {};
|
||||
|
||||
// Удалить из хранилища
|
||||
if (this.container.id && instances[this.container.id]) {
|
||||
delete instances[this.container.id];
|
||||
}
|
||||
};
|
||||
|
||||
// ========== СТАТИЧЕСКИЕ МЕТОДЫ ==========
|
||||
|
||||
/**
|
||||
* Инициализация компонента
|
||||
* @param {string|Element} container - селектор или DOM элемент
|
||||
* @param {Object} options - настройки
|
||||
* @returns {ProductSearchPicker}
|
||||
*/
|
||||
ProductSearchPicker.init = function(container, options) {
|
||||
var element = typeof container === 'string'
|
||||
? document.querySelector(container)
|
||||
: container;
|
||||
|
||||
if (!element) {
|
||||
console.error('ProductSearchPicker.init: container not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Проверяем, не инициализирован ли уже
|
||||
if (element.id && instances[element.id]) {
|
||||
return instances[element.id];
|
||||
}
|
||||
|
||||
var instance = new ProductSearchPicker(element, options);
|
||||
|
||||
if (element.id) {
|
||||
instances[element.id] = instance;
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить экземпляр по контейнеру
|
||||
*/
|
||||
ProductSearchPicker.getInstance = function(container) {
|
||||
var element = typeof container === 'string'
|
||||
? document.querySelector(container)
|
||||
: container;
|
||||
return element && element.id ? instances[element.id] : null;
|
||||
};
|
||||
|
||||
// Экспорт в глобальную область
|
||||
window.ProductSearchPicker = ProductSearchPicker;
|
||||
|
||||
})(window);
|
||||
Reference in New Issue
Block a user