Add AJAX filtering for preview popover with width lock

- Convert preview filter from client-side JS to server-side AJAX
- Add debounce utility function for 300ms delayed AJAX calls
- Add filter handlers for all 4 preview types (tab, condition, group, filter-group)
- Add filterProductIdsByQuery() method to EntityPreviewHandler
- Update all AJAX handlers to support filter parameter
- Lock popover width when filtering to prevent resize
- Add loading overlay CSS for filter state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 14:07:56 +01:00
parent 6f248605a7
commit b79a89bbb4
11 changed files with 917 additions and 80 deletions

View File

@@ -332,6 +332,7 @@ trait EntitySelector
$blockType = Tools::getValue('block_type', 'products');
$limit = (int) Tools::getValue('limit', 10);
$offset = (int) Tools::getValue('offset', 0);
$filter = Tools::getValue('filter', '');
$values = json_decode($valuesJson, true);
if (!is_array($values)) {
@@ -344,12 +345,19 @@ trait EntitySelector
try {
if ($blockType === 'products') {
$matchingIds = $this->getProductConditionResolver()->getIdsByMethod($method, $values);
// Apply filter if provided
$previewHandler = $this->getEntityPreviewHandler();
if (!empty($filter)) {
$matchingIds = $previewHandler->filterProductIdsByQuery($matchingIds, $filter, $idLang, $idShop);
}
$totalCount = count($matchingIds);
$previewItems = [];
if ($totalCount > 0 && $limit > 0) {
$previewIds = array_slice($matchingIds, $offset, $limit);
$previewItems = $this->getEntityPreviewHandler()->getProductPreviewData($previewIds, $idLang, $idShop);
$previewItems = $previewHandler->getProductPreviewData($previewIds, $idLang, $idShop);
}
$this->ajaxDie(json_encode([
@@ -386,6 +394,7 @@ trait EntitySelector
$blockType = Tools::getValue('block_type', 'products');
$limit = (int) Tools::getValue('limit', 10);
$offset = (int) Tools::getValue('offset', 0);
$filter = Tools::getValue('filter', '');
$groupData = json_decode($groupDataJson, true);
if (!is_array($groupData) || !isset($groupData['include'])) {
@@ -482,6 +491,11 @@ trait EntitySelector
$matchingIds = $resolver->applyModifiers($matchingIds, $modifiers);
}
// Apply filter if provided
if (!empty($filter)) {
$matchingIds = $previewHandler->filterProductIdsByQuery($matchingIds, $filter, $idLang, $idShop);
}
$totalCount = count($matchingIds);
$previewItems = [];
if ($totalCount > 0 && $limit > 0) {
@@ -595,6 +609,7 @@ trait EntitySelector
$groupId = (int) Tools::getValue('group_id');
$groupType = Tools::getValue('group_type'); // 'attribute' or 'feature'
$limit = (int) Tools::getValue('limit', 10);
$filter = Tools::getValue('filter', '');
if (!$groupId || !in_array($groupType, ['attribute', 'feature'])) {
die(json_encode([
@@ -630,30 +645,35 @@ trait EntitySelector
$sql->where('ps.active = 1');
}
// Get total count first
$countSql = clone $sql;
$countResult = $db->executeS($countSql);
$totalCount = count($countResult);
// Get limited results for preview
$sql->limit($limit);
// Get all matching product IDs
$results = $db->executeS($sql);
$productIds = array_column($results, 'id_product');
// Apply filter if provided
if (!empty($filter) && !empty($productIds)) {
$productIds = $this->getEntityPreviewHandler()->filterProductIdsByQuery($productIds, $filter, $idLang, $idShop);
}
$totalCount = count($productIds);
// Get limited results for preview
$previewIds = array_slice($productIds, 0, $limit);
// Get product details for preview
$items = [];
if (!empty($productIds)) {
if (!empty($previewIds)) {
$productSql = new DbQuery();
$productSql->select('p.id_product, pl.name, p.reference, i.id_image, m.name as manufacturer');
$productSql->from('product', 'p');
$productSql->innerJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . $idLang . ' AND pl.id_shop = ' . $idShop);
$productSql->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
$productSql->leftJoin('manufacturer', 'm', 'm.id_manufacturer = p.id_manufacturer');
$productSql->where('p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ')');
$productSql->limit($limit);
$productSql->where('p.id_product IN (' . implode(',', array_map('intval', $previewIds)) . ')');
$products = $db->executeS($productSql);
// Map products by ID for ordering
$productsById = [];
foreach ($products as $product) {
$imageUrl = null;
if ($product['id_image']) {
@@ -664,7 +684,7 @@ trait EntitySelector
);
}
$items[] = [
$productsById[(int) $product['id_product']] = [
'id' => (int) $product['id_product'],
'name' => $product['name'],
'reference' => $product['reference'],
@@ -672,6 +692,13 @@ trait EntitySelector
'image' => $imageUrl
];
}
// Preserve order from previewIds
foreach ($previewIds as $id) {
if (isset($productsById[(int) $id])) {
$items[] = $productsById[(int) $id];
}
}
}
die(json_encode([

View File

@@ -211,6 +211,63 @@ class EntityPreviewHandler
return $this->buildPreviewResponse($allIds, $items, $limit, $offset);
}
/**
* Filter product IDs by search query (name, reference)
*
* @param array $productIds Product IDs to filter
* @param string $query Search query
* @param int $idLang Language ID
* @param int $idShop Shop ID
* @return array Filtered product IDs (preserves order)
*/
public function filterProductIdsByQuery(array $productIds, $query, $idLang, $idShop)
{
if (empty($productIds) || empty($query)) {
return $productIds;
}
$query = trim($query);
if (strlen($query) < 1) {
return $productIds;
}
$escapedQuery = '%' . pSQL($query) . '%';
$sql = new DbQuery();
$sql->select('DISTINCT p.id_product');
$sql->from('product', 'p');
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
$sql->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
$sql->leftJoin('manufacturer', 'm', 'm.id_manufacturer = p.id_manufacturer');
$sql->where('p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ')');
$sql->where('(
pl.name LIKE \'' . $escapedQuery . '\'
OR p.reference LIKE \'' . $escapedQuery . '\'
OR m.name LIKE \'' . $escapedQuery . '\'
)');
$results = Db::getInstance()->executeS($sql);
if (!$results) {
return [];
}
$matchingIds = [];
foreach ($results as $row) {
$matchingIds[(int) $row['id_product']] = true;
}
// Preserve original order
$filtered = [];
foreach ($productIds as $id) {
if (isset($matchingIds[(int) $id])) {
$filtered[] = (int) $id;
}
}
return $filtered;
}
/**
* Get product preview data
*