Add clickable preview popover to filter group toggles

- Add showFilterGroupPreviewPopover method in _preview.js
- Make toggle-count badges clickable with data attributes
- Add event binding for .toggle-count.clickable in _events.js
- Add hover/active/loading styles for clickable toggle-count
- Requires previewFilterGroupProducts AJAX handler in PHP backend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 10:18:42 +00:00
parent aa9f28bb7e
commit 6ebf94e15b
9 changed files with 306 additions and 14 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -231,6 +231,23 @@
} }
}); });
// Filter group toggle count badge click for preview popover
$(document).on('click', '.filter-group-toggle .toggle-count.clickable', function(e) {
e.stopPropagation();
e.preventDefault();
var $badge = $(this);
var groupId = $badge.data('groupId');
var groupType = $badge.data('type');
var groupName = $badge.data('groupName');
if ($badge.hasClass('popover-open')) {
self.hidePreviewPopover();
} else {
self.showFilterGroupPreviewPopover($badge, groupId, groupType, groupName);
}
});
// Close popover when clicking outside // Close popover when clicking outside
$(document).on('click', function(e) { $(document).on('click', function(e) {
if (!$(e.target).closest('.target-preview-popover').length && if (!$(e.target).closest('.target-preview-popover').length &&
@@ -238,7 +255,8 @@
!$(e.target).closest('.condition-match-count').length && !$(e.target).closest('.condition-match-count').length &&
!$(e.target).closest('.group-count-badge').length && !$(e.target).closest('.group-count-badge').length &&
!$(e.target).closest('.group-modifiers').length && !$(e.target).closest('.group-modifiers').length &&
!$(e.target).closest('.group-preview-badge').length) { !$(e.target).closest('.group-preview-badge').length &&
!$(e.target).closest('.toggle-count.clickable').length) {
self.hidePreviewPopover(); self.hidePreviewPopover();
} }
}); });
@@ -3501,10 +3519,10 @@
if (this.filterableData.attributes && this.filterableData.attributes.length > 0) { if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
this.filterableData.attributes.forEach(function(group) { this.filterableData.attributes.forEach(function(group) {
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute">'; var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>'; html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) { if (group.count !== undefined) {
html += '<span class="toggle-count"><i class="icon-eye"></i> (' + group.count + ')</span>'; html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '"><i class="icon-eye"></i> ' + group.count + '</span>';
} }
html += '</button>'; html += '</button>';
$attrContainer.append(html); $attrContainer.append(html);
@@ -3518,10 +3536,10 @@
if (this.filterableData.features && this.filterableData.features.length > 0) { if (this.filterableData.features && this.filterableData.features.length > 0) {
this.filterableData.features.forEach(function(group) { this.filterableData.features.forEach(function(group) {
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature">'; var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>'; html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) { if (group.count !== undefined) {
html += '<span class="toggle-count"><i class="icon-eye"></i> (' + group.count + ')</span>'; html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '"><i class="icon-eye"></i> ' + group.count + '</span>';
} }
html += '</button>'; html += '</button>';
$featContainer.append(html); $featContainer.append(html);
@@ -7580,6 +7598,118 @@
price = parseFloat(price) || 0; price = parseFloat(price) || 0;
} }
return price.toFixed(2) + ' €'; return price.toFixed(2) + ' €';
},
/**
* Show preview popover for filter group toggle (attribute/feature groups)
*/
showFilterGroupPreviewPopover: function($badge, groupId, groupType, groupName) {
var self = this;
this.hidePreviewPopover();
$badge.addClass('popover-open loading');
this.$activeBadge = $badge;
var trans = this.config.trans || {};
var entityLabelPlural = 'products';
// Fetch products matching this attribute/feature group
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewFilterGroupProducts',
trait: 'EntitySelector',
group_id: groupId,
group_type: groupType,
limit: 10
},
success: function(response) {
$badge.removeClass('loading');
if (response.success) {
var items = response.items || [];
var totalCount = response.count || 0;
var hasMore = response.hasMore || false;
self.showFilterGroupItemsPopover($badge, items, totalCount, hasMore, entityLabelPlural, groupName, groupType);
} else {
$badge.removeClass('popover-open');
self.$activeBadge = null;
}
},
error: function() {
$badge.removeClass('loading popover-open');
self.$activeBadge = null;
}
});
},
/**
* Show popover for filter group preview items
*/
showFilterGroupItemsPopover: function($badge, items, totalCount, hasMore, entityLabel, groupName, groupType) {
var self = this;
var trans = this.config.trans || {};
var typeLabel = groupType === 'attribute' ? (trans.attribute || 'Attribute') : (trans.feature || 'Feature');
var html = '<div class="target-preview-popover preview-type-filter-group">';
html += '<div class="preview-header">';
html += '<span class="preview-count">' + totalCount + ' ' + entityLabel + '</span>';
html += '<button type="button" class="preview-close"><i class="icon-times"></i></button>';
html += '</div>';
if (items.length > 0) {
html += '<div class="preview-list">';
html += this.renderPreviewItems(items);
html += '</div>';
if (hasMore) {
var remaining = totalCount - items.length;
html += '<div class="preview-footer">';
html += '<span class="preview-more-info">+ ' + remaining + ' ' + (trans.more || 'more') + '</span>';
html += '</div>';
}
} else {
html += '<div class="preview-empty">' + (trans.no_preview || 'No items to preview') + '</div>';
}
html += '</div>';
var $popover = $(html);
$('body').append($popover);
$popover.find('.preview-close').on('click', function() {
self.hidePreviewPopover();
});
// Position popover below badge
var badgeOffset = $badge.offset();
var badgeHeight = $badge.outerHeight();
var badgeWidth = $badge.outerWidth();
var popoverWidth = $popover.outerWidth();
var leftPos = badgeOffset.left + (badgeWidth / 2) - (popoverWidth / 2);
var minLeft = 10;
var maxLeft = $(window).width() - popoverWidth - 10;
leftPos = Math.max(minLeft, Math.min(leftPos, maxLeft));
var topPos = badgeOffset.top + badgeHeight + 8;
$popover.css({
position: 'absolute',
top: topPos,
left: leftPos,
zIndex: 10000
});
$popover.addClass('show');
this.$previewPopover = $popover;
} }
}; };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -91,6 +91,23 @@
} }
}); });
// Filter group toggle count badge click for preview popover
$(document).on('click', '.filter-group-toggle .toggle-count.clickable', function(e) {
e.stopPropagation();
e.preventDefault();
var $badge = $(this);
var groupId = $badge.data('groupId');
var groupType = $badge.data('type');
var groupName = $badge.data('groupName');
if ($badge.hasClass('popover-open')) {
self.hidePreviewPopover();
} else {
self.showFilterGroupPreviewPopover($badge, groupId, groupType, groupName);
}
});
// Close popover when clicking outside // Close popover when clicking outside
$(document).on('click', function(e) { $(document).on('click', function(e) {
if (!$(e.target).closest('.target-preview-popover').length && if (!$(e.target).closest('.target-preview-popover').length &&
@@ -98,7 +115,8 @@
!$(e.target).closest('.condition-match-count').length && !$(e.target).closest('.condition-match-count').length &&
!$(e.target).closest('.group-count-badge').length && !$(e.target).closest('.group-count-badge').length &&
!$(e.target).closest('.group-modifiers').length && !$(e.target).closest('.group-modifiers').length &&
!$(e.target).closest('.group-preview-badge').length) { !$(e.target).closest('.group-preview-badge').length &&
!$(e.target).closest('.toggle-count.clickable').length) {
self.hidePreviewPopover(); self.hidePreviewPopover();
} }
}); });

View File

@@ -161,10 +161,10 @@
if (this.filterableData.attributes && this.filterableData.attributes.length > 0) { if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
this.filterableData.attributes.forEach(function(group) { this.filterableData.attributes.forEach(function(group) {
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute">'; var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>'; html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) { if (group.count !== undefined) {
html += '<span class="toggle-count"><i class="icon-eye"></i> (' + group.count + ')</span>'; html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '"><i class="icon-eye"></i> ' + group.count + '</span>';
} }
html += '</button>'; html += '</button>';
$attrContainer.append(html); $attrContainer.append(html);
@@ -178,10 +178,10 @@
if (this.filterableData.features && this.filterableData.features.length > 0) { if (this.filterableData.features && this.filterableData.features.length > 0) {
this.filterableData.features.forEach(function(group) { this.filterableData.features.forEach(function(group) {
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature">'; var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>'; html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) { if (group.count !== undefined) {
html += '<span class="toggle-count"><i class="icon-eye"></i> (' + group.count + ')</span>'; html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '"><i class="icon-eye"></i> ' + group.count + '</span>';
} }
html += '</button>'; html += '</button>';
$featContainer.append(html); $featContainer.append(html);

View File

@@ -650,6 +650,118 @@
price = parseFloat(price) || 0; price = parseFloat(price) || 0;
} }
return price.toFixed(2) + ' €'; return price.toFixed(2) + ' €';
},
/**
* Show preview popover for filter group toggle (attribute/feature groups)
*/
showFilterGroupPreviewPopover: function($badge, groupId, groupType, groupName) {
var self = this;
this.hidePreviewPopover();
$badge.addClass('popover-open loading');
this.$activeBadge = $badge;
var trans = this.config.trans || {};
var entityLabelPlural = 'products';
// Fetch products matching this attribute/feature group
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewFilterGroupProducts',
trait: 'EntitySelector',
group_id: groupId,
group_type: groupType,
limit: 10
},
success: function(response) {
$badge.removeClass('loading');
if (response.success) {
var items = response.items || [];
var totalCount = response.count || 0;
var hasMore = response.hasMore || false;
self.showFilterGroupItemsPopover($badge, items, totalCount, hasMore, entityLabelPlural, groupName, groupType);
} else {
$badge.removeClass('popover-open');
self.$activeBadge = null;
}
},
error: function() {
$badge.removeClass('loading popover-open');
self.$activeBadge = null;
}
});
},
/**
* Show popover for filter group preview items
*/
showFilterGroupItemsPopover: function($badge, items, totalCount, hasMore, entityLabel, groupName, groupType) {
var self = this;
var trans = this.config.trans || {};
var typeLabel = groupType === 'attribute' ? (trans.attribute || 'Attribute') : (trans.feature || 'Feature');
var html = '<div class="target-preview-popover preview-type-filter-group">';
html += '<div class="preview-header">';
html += '<span class="preview-count">' + totalCount + ' ' + entityLabel + '</span>';
html += '<button type="button" class="preview-close"><i class="icon-times"></i></button>';
html += '</div>';
if (items.length > 0) {
html += '<div class="preview-list">';
html += this.renderPreviewItems(items);
html += '</div>';
if (hasMore) {
var remaining = totalCount - items.length;
html += '<div class="preview-footer">';
html += '<span class="preview-more-info">+ ' + remaining + ' ' + (trans.more || 'more') + '</span>';
html += '</div>';
}
} else {
html += '<div class="preview-empty">' + (trans.no_preview || 'No items to preview') + '</div>';
}
html += '</div>';
var $popover = $(html);
$('body').append($popover);
$popover.find('.preview-close').on('click', function() {
self.hidePreviewPopover();
});
// Position popover below badge
var badgeOffset = $badge.offset();
var badgeHeight = $badge.outerHeight();
var badgeWidth = $badge.outerWidth();
var popoverWidth = $popover.outerWidth();
var leftPos = badgeOffset.left + (badgeWidth / 2) - (popoverWidth / 2);
var minLeft = 10;
var maxLeft = $(window).width() - popoverWidth - 10;
leftPos = Math.max(minLeft, Math.min(leftPos, maxLeft));
var topPos = badgeOffset.top + badgeHeight + 8;
$popover.css({
position: 'absolute',
top: topPos,
left: leftPos,
zIndex: 10000
});
$popover.addClass('show');
this.$previewPopover = $popover;
} }
}; };

View File

@@ -1242,6 +1242,38 @@ body > .target-search-dropdown,
font-size: 10px; font-size: 10px;
color: $es-primary; color: $es-primary;
} }
// Clickable preview badge
&.clickable {
cursor: pointer;
padding: 0.125rem 0.25rem;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: rgba($es-primary, 0.1);
color: $es-primary;
i {
color: $es-primary;
}
}
&.popover-open {
background: $es-primary;
color: $es-white;
i {
color: $es-white;
}
}
&.loading {
i {
animation: spin 0.6s linear infinite;
}
}
}
} }
} }