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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -22,6 +22,24 @@
|
||||
// Utility functions mixin
|
||||
window._EntitySelectorMixins.utils = {
|
||||
|
||||
/**
|
||||
* Debounce function - delays execution until after wait milliseconds
|
||||
* @param {Function} func - Function to debounce
|
||||
* @param {number} wait - Milliseconds to wait
|
||||
* @returns {Function} Debounced function
|
||||
*/
|
||||
debounce: function(func, wait) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(function() {
|
||||
func.apply(context, args);
|
||||
}, wait);
|
||||
};
|
||||
},
|
||||
|
||||
escapeHtml: function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
@@ -1784,7 +1802,8 @@
|
||||
// Click outside to close
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.value-picker').length &&
|
||||
!$(e.target).closest('.target-search-dropdown').length) {
|
||||
!$(e.target).closest('.target-search-dropdown').length &&
|
||||
!$(e.target).closest('.target-preview-popover').length) {
|
||||
self.hideDropdown();
|
||||
}
|
||||
});
|
||||
@@ -7050,6 +7069,7 @@
|
||||
* @param {string} options.entityLabel - Label for items (e.g., "products")
|
||||
* @param {string} options.previewType - Type identifier (e.g., "condition", "filter-group")
|
||||
* @param {Function} options.onLoadMore - Callback when load more is clicked
|
||||
* @param {Function} options.onFilter - Callback for AJAX filtering (receives query string)
|
||||
* @param {Object} options.context - Context data for load more
|
||||
*/
|
||||
createPreviewPopover: function(options) {
|
||||
@@ -7118,16 +7138,37 @@
|
||||
this.previewTotalCount = totalCount;
|
||||
this.previewContext = options.context || {};
|
||||
this.previewOnLoadMore = options.onLoadMore || null;
|
||||
this.previewOnFilter = options.onFilter || null;
|
||||
this.previewCurrentFilter = '';
|
||||
this.previewEntityLabel = entityLabel;
|
||||
|
||||
// Event handlers
|
||||
$popover.find('.preview-close').on('click', function() {
|
||||
self.hidePreviewPopover();
|
||||
});
|
||||
|
||||
$popover.find('.preview-filter-input').on('input', function() {
|
||||
var query = $(this).val().toLowerCase().trim();
|
||||
self.filterPreviewItems(query);
|
||||
});
|
||||
// Filter input with AJAX support
|
||||
var $filterInput = $popover.find('.preview-filter-input');
|
||||
if (options.onFilter) {
|
||||
// Use AJAX filtering with debounce
|
||||
var debouncedFilter = this.debounce(function(query) {
|
||||
self.previewCurrentFilter = query;
|
||||
self.showFilterLoading(true);
|
||||
options.onFilter.call(self, query);
|
||||
}, 300);
|
||||
|
||||
$filterInput.on('input', function() {
|
||||
var query = $(this).val().trim();
|
||||
if (query === self.previewCurrentFilter) return;
|
||||
debouncedFilter(query);
|
||||
});
|
||||
} else {
|
||||
// Fallback to client-side filtering
|
||||
$filterInput.on('input', function() {
|
||||
var query = $(this).val().toLowerCase().trim();
|
||||
self.filterPreviewItems(query);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.onLoadMore) {
|
||||
$popover.find('.btn-load-more').on('click', function() {
|
||||
@@ -7290,7 +7331,7 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter preview items by query
|
||||
* Filter preview items by query (client-side fallback)
|
||||
*/
|
||||
filterPreviewItems: function(query) {
|
||||
if (!this.$previewList) return;
|
||||
@@ -7316,6 +7357,133 @@
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show/hide loading indicator during AJAX filter
|
||||
*/
|
||||
showFilterLoading: function(show) {
|
||||
if (!this.$previewPopover) return;
|
||||
|
||||
var $list = this.$previewList;
|
||||
if (!$list) return;
|
||||
|
||||
if (show) {
|
||||
// Lock the popover width before filtering to prevent resize
|
||||
if (!this.previewLockedWidth) {
|
||||
this.previewLockedWidth = this.$previewPopover.outerWidth();
|
||||
this.$previewPopover.css('width', this.previewLockedWidth + 'px');
|
||||
}
|
||||
|
||||
$list.addClass('filtering');
|
||||
// Add overlay if not exists
|
||||
if (!$list.find('.filter-loading-overlay').length) {
|
||||
$list.append('<div class="filter-loading-overlay"><i class="icon-spinner icon-spin"></i></div>');
|
||||
}
|
||||
} else {
|
||||
$list.removeClass('filtering');
|
||||
$list.find('.filter-loading-overlay').remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update preview popover with filtered AJAX results
|
||||
* @param {Object} response - AJAX response with items, count, hasMore
|
||||
*/
|
||||
updatePreviewPopoverFiltered: function(response) {
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
this.showFilterLoading(false);
|
||||
|
||||
if (!response.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
var items = response.items || [];
|
||||
var filteredCount = response.count || 0;
|
||||
var hasMore = response.hasMore || false;
|
||||
|
||||
// Update header count to show filtered count
|
||||
var $header = this.$previewPopover.find('.preview-header');
|
||||
var entityLabel = this.previewEntityLabel || 'items';
|
||||
$header.find('.preview-count').text(filteredCount + ' ' + entityLabel);
|
||||
|
||||
// Update list
|
||||
if (items.length > 0) {
|
||||
this.$previewList.html(this.renderPreviewItems(items));
|
||||
this.previewLoadedCount = items.length;
|
||||
this.previewTotalCount = filteredCount;
|
||||
} else {
|
||||
var noResultsText = trans.no_filter_results || 'No matching items found';
|
||||
this.$previewList.html('<div class="preview-empty">' + noResultsText + '</div>');
|
||||
this.previewLoadedCount = 0;
|
||||
this.previewTotalCount = 0;
|
||||
}
|
||||
|
||||
// Update or create footer for load more
|
||||
var $footer = this.$previewPopover.find('.preview-footer');
|
||||
if (hasMore && items.length > 0) {
|
||||
var remaining = filteredCount - items.length;
|
||||
if ($footer.length) {
|
||||
var $controls = $footer.find('.load-more-controls');
|
||||
var $btn = $controls.find('.btn-load-more');
|
||||
var $select = $controls.find('.load-more-select');
|
||||
|
||||
$btn.removeClass('loading');
|
||||
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
|
||||
$select.prop('disabled', false);
|
||||
$controls.find('.remaining-count').text(remaining);
|
||||
|
||||
$select.empty();
|
||||
if (remaining >= 10) $select.append('<option value="10">10</option>');
|
||||
if (remaining >= 20) $select.append('<option value="20" selected>20</option>');
|
||||
if (remaining >= 50) $select.append('<option value="50">50</option>');
|
||||
if (remaining >= 100) $select.append('<option value="100">100</option>');
|
||||
$select.append('<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>');
|
||||
} else {
|
||||
// Create footer
|
||||
var footerHtml = '<div class="preview-footer">';
|
||||
footerHtml += '<div class="load-more-controls">';
|
||||
footerHtml += '<span class="load-more-label">' + (trans.load || 'Load') + '</span>';
|
||||
footerHtml += '<select class="load-more-select">';
|
||||
if (remaining >= 10) footerHtml += '<option value="10">10</option>';
|
||||
if (remaining >= 20) footerHtml += '<option value="20" selected>20</option>';
|
||||
if (remaining >= 50) footerHtml += '<option value="50">50</option>';
|
||||
if (remaining >= 100) footerHtml += '<option value="100">100</option>';
|
||||
footerHtml += '<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>';
|
||||
footerHtml += '</select>';
|
||||
footerHtml += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">' + remaining + '</span> ' + (trans.remaining || 'remaining') + '</span>';
|
||||
footerHtml += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
|
||||
footerHtml += '</div>';
|
||||
footerHtml += '</div>';
|
||||
|
||||
var $newFooter = $(footerHtml);
|
||||
this.$previewList.after($newFooter);
|
||||
|
||||
// Rebind load more click
|
||||
var self = this;
|
||||
if (this.previewOnLoadMore) {
|
||||
$newFooter.find('.btn-load-more').on('click', function() {
|
||||
var $btn = $(this);
|
||||
var $controls = $btn.closest('.load-more-controls');
|
||||
var $select = $controls.find('.load-more-select');
|
||||
|
||||
if ($btn.hasClass('loading')) return;
|
||||
|
||||
$btn.addClass('loading');
|
||||
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
|
||||
$select.prop('disabled', true);
|
||||
|
||||
var loadCount = parseInt($select.val(), 10) || 20;
|
||||
self.previewLoadCount = loadCount;
|
||||
|
||||
self.previewOnLoadMore.call(self, $btn);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$footer.remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
@@ -7351,6 +7519,10 @@
|
||||
this.$previewList = null;
|
||||
this.previewContext = null;
|
||||
this.previewOnLoadMore = null;
|
||||
this.previewOnFilter = null;
|
||||
this.previewCurrentFilter = '';
|
||||
this.previewEntityLabel = null;
|
||||
this.previewLockedWidth = null;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
@@ -7388,6 +7560,57 @@
|
||||
context: { $tab: $tab, blockType: blockType },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreTabPreviewItems($tab, $btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterTabPreviewItems($tab, query);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* AJAX filter handler for tab preview
|
||||
*/
|
||||
filterTabPreviewItems: function($tab, query) {
|
||||
var self = this;
|
||||
var blockType = this.previewBlockType;
|
||||
|
||||
var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
|
||||
var savedData = {};
|
||||
try {
|
||||
savedData = JSON.parse($hiddenInput.val() || '{}');
|
||||
} catch (e) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
|
||||
if (groups.length === 0) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data[blockType] = { groups: groups };
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'previewTargetConditions',
|
||||
trait: 'TargetConditions',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
filter: query,
|
||||
limit: 20,
|
||||
offset: 0
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -7412,19 +7635,25 @@
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewTargetConditions',
|
||||
trait: 'TargetConditions',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
limit: self.previewLoadedCount + loadCount,
|
||||
offset: 0
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'previewTargetConditions',
|
||||
trait: 'TargetConditions',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
limit: self.previewLoadedCount + loadCount,
|
||||
offset: 0
|
||||
},
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
var items = response.items || response.products || [];
|
||||
if (response.success && items.length > 0) {
|
||||
@@ -7491,6 +7720,9 @@
|
||||
context: { conditionData: conditionData, blockType: blockType },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreConditionItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterConditionItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -7505,13 +7737,17 @@
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreConditionItems: function($btn) {
|
||||
/**
|
||||
* AJAX filter handler for condition preview
|
||||
*/
|
||||
filterConditionItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.conditionData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
if (!ctx || !ctx.conditionData) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -7524,8 +7760,45 @@
|
||||
method: ctx.conditionData.method,
|
||||
values: JSON.stringify(ctx.conditionData.values),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
filter: query,
|
||||
limit: 20
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreConditionItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.conditionData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewConditionItems',
|
||||
trait: 'EntitySelector',
|
||||
method: ctx.conditionData.method,
|
||||
values: JSON.stringify(ctx.conditionData.values),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.previewTotalCount = response.count;
|
||||
@@ -7602,6 +7875,9 @@
|
||||
context: { groupData: groupData, blockType: blockType, $group: $group },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreGroupItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterGroupItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -7616,13 +7892,17 @@
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreGroupItems: function($btn) {
|
||||
/**
|
||||
* AJAX filter handler for group preview
|
||||
*/
|
||||
filterGroupItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
if (!ctx || !ctx.groupData) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -7634,8 +7914,44 @@
|
||||
trait: 'EntitySelector',
|
||||
group_data: JSON.stringify(ctx.groupData),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
filter: query,
|
||||
limit: 20
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreGroupItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewGroupItems',
|
||||
trait: 'EntitySelector',
|
||||
group_data: JSON.stringify(ctx.groupData),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.previewTotalCount = response.count;
|
||||
@@ -7690,6 +8006,9 @@
|
||||
context: { groupId: groupId, groupType: groupType, groupName: groupName },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreFilterGroupItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterFilterGroupItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -7704,13 +8023,17 @@
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreFilterGroupItems: function($btn) {
|
||||
/**
|
||||
* AJAX filter handler for filter group preview
|
||||
*/
|
||||
filterFilterGroupItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupId) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
if (!ctx || !ctx.groupId) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -7722,8 +8045,44 @@
|
||||
trait: 'EntitySelector',
|
||||
group_id: ctx.groupId,
|
||||
group_type: ctx.groupType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
filter: query,
|
||||
limit: 20
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreFilterGroupItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupId) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewFilterGroupProducts',
|
||||
trait: 'EntitySelector',
|
||||
group_id: ctx.groupId,
|
||||
group_type: ctx.groupType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.previewTotalCount = response.count;
|
||||
|
||||
2
assets/js/admin/entity-selector.min.js
vendored
2
assets/js/admin/entity-selector.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1644,7 +1644,8 @@
|
||||
// Click outside to close
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.value-picker').length &&
|
||||
!$(e.target).closest('.target-search-dropdown').length) {
|
||||
!$(e.target).closest('.target-search-dropdown').length &&
|
||||
!$(e.target).closest('.target-preview-popover').length) {
|
||||
self.hideDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
* @param {string} options.entityLabel - Label for items (e.g., "products")
|
||||
* @param {string} options.previewType - Type identifier (e.g., "condition", "filter-group")
|
||||
* @param {Function} options.onLoadMore - Callback when load more is clicked
|
||||
* @param {Function} options.onFilter - Callback for AJAX filtering (receives query string)
|
||||
* @param {Object} options.context - Context data for load more
|
||||
*/
|
||||
createPreviewPopover: function(options) {
|
||||
@@ -131,16 +132,37 @@
|
||||
this.previewTotalCount = totalCount;
|
||||
this.previewContext = options.context || {};
|
||||
this.previewOnLoadMore = options.onLoadMore || null;
|
||||
this.previewOnFilter = options.onFilter || null;
|
||||
this.previewCurrentFilter = '';
|
||||
this.previewEntityLabel = entityLabel;
|
||||
|
||||
// Event handlers
|
||||
$popover.find('.preview-close').on('click', function() {
|
||||
self.hidePreviewPopover();
|
||||
});
|
||||
|
||||
$popover.find('.preview-filter-input').on('input', function() {
|
||||
var query = $(this).val().toLowerCase().trim();
|
||||
self.filterPreviewItems(query);
|
||||
});
|
||||
// Filter input with AJAX support
|
||||
var $filterInput = $popover.find('.preview-filter-input');
|
||||
if (options.onFilter) {
|
||||
// Use AJAX filtering with debounce
|
||||
var debouncedFilter = this.debounce(function(query) {
|
||||
self.previewCurrentFilter = query;
|
||||
self.showFilterLoading(true);
|
||||
options.onFilter.call(self, query);
|
||||
}, 300);
|
||||
|
||||
$filterInput.on('input', function() {
|
||||
var query = $(this).val().trim();
|
||||
if (query === self.previewCurrentFilter) return;
|
||||
debouncedFilter(query);
|
||||
});
|
||||
} else {
|
||||
// Fallback to client-side filtering
|
||||
$filterInput.on('input', function() {
|
||||
var query = $(this).val().toLowerCase().trim();
|
||||
self.filterPreviewItems(query);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.onLoadMore) {
|
||||
$popover.find('.btn-load-more').on('click', function() {
|
||||
@@ -303,7 +325,7 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter preview items by query
|
||||
* Filter preview items by query (client-side fallback)
|
||||
*/
|
||||
filterPreviewItems: function(query) {
|
||||
if (!this.$previewList) return;
|
||||
@@ -329,6 +351,133 @@
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show/hide loading indicator during AJAX filter
|
||||
*/
|
||||
showFilterLoading: function(show) {
|
||||
if (!this.$previewPopover) return;
|
||||
|
||||
var $list = this.$previewList;
|
||||
if (!$list) return;
|
||||
|
||||
if (show) {
|
||||
// Lock the popover width before filtering to prevent resize
|
||||
if (!this.previewLockedWidth) {
|
||||
this.previewLockedWidth = this.$previewPopover.outerWidth();
|
||||
this.$previewPopover.css('width', this.previewLockedWidth + 'px');
|
||||
}
|
||||
|
||||
$list.addClass('filtering');
|
||||
// Add overlay if not exists
|
||||
if (!$list.find('.filter-loading-overlay').length) {
|
||||
$list.append('<div class="filter-loading-overlay"><i class="icon-spinner icon-spin"></i></div>');
|
||||
}
|
||||
} else {
|
||||
$list.removeClass('filtering');
|
||||
$list.find('.filter-loading-overlay').remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update preview popover with filtered AJAX results
|
||||
* @param {Object} response - AJAX response with items, count, hasMore
|
||||
*/
|
||||
updatePreviewPopoverFiltered: function(response) {
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
this.showFilterLoading(false);
|
||||
|
||||
if (!response.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
var items = response.items || [];
|
||||
var filteredCount = response.count || 0;
|
||||
var hasMore = response.hasMore || false;
|
||||
|
||||
// Update header count to show filtered count
|
||||
var $header = this.$previewPopover.find('.preview-header');
|
||||
var entityLabel = this.previewEntityLabel || 'items';
|
||||
$header.find('.preview-count').text(filteredCount + ' ' + entityLabel);
|
||||
|
||||
// Update list
|
||||
if (items.length > 0) {
|
||||
this.$previewList.html(this.renderPreviewItems(items));
|
||||
this.previewLoadedCount = items.length;
|
||||
this.previewTotalCount = filteredCount;
|
||||
} else {
|
||||
var noResultsText = trans.no_filter_results || 'No matching items found';
|
||||
this.$previewList.html('<div class="preview-empty">' + noResultsText + '</div>');
|
||||
this.previewLoadedCount = 0;
|
||||
this.previewTotalCount = 0;
|
||||
}
|
||||
|
||||
// Update or create footer for load more
|
||||
var $footer = this.$previewPopover.find('.preview-footer');
|
||||
if (hasMore && items.length > 0) {
|
||||
var remaining = filteredCount - items.length;
|
||||
if ($footer.length) {
|
||||
var $controls = $footer.find('.load-more-controls');
|
||||
var $btn = $controls.find('.btn-load-more');
|
||||
var $select = $controls.find('.load-more-select');
|
||||
|
||||
$btn.removeClass('loading');
|
||||
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
|
||||
$select.prop('disabled', false);
|
||||
$controls.find('.remaining-count').text(remaining);
|
||||
|
||||
$select.empty();
|
||||
if (remaining >= 10) $select.append('<option value="10">10</option>');
|
||||
if (remaining >= 20) $select.append('<option value="20" selected>20</option>');
|
||||
if (remaining >= 50) $select.append('<option value="50">50</option>');
|
||||
if (remaining >= 100) $select.append('<option value="100">100</option>');
|
||||
$select.append('<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>');
|
||||
} else {
|
||||
// Create footer
|
||||
var footerHtml = '<div class="preview-footer">';
|
||||
footerHtml += '<div class="load-more-controls">';
|
||||
footerHtml += '<span class="load-more-label">' + (trans.load || 'Load') + '</span>';
|
||||
footerHtml += '<select class="load-more-select">';
|
||||
if (remaining >= 10) footerHtml += '<option value="10">10</option>';
|
||||
if (remaining >= 20) footerHtml += '<option value="20" selected>20</option>';
|
||||
if (remaining >= 50) footerHtml += '<option value="50">50</option>';
|
||||
if (remaining >= 100) footerHtml += '<option value="100">100</option>';
|
||||
footerHtml += '<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>';
|
||||
footerHtml += '</select>';
|
||||
footerHtml += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">' + remaining + '</span> ' + (trans.remaining || 'remaining') + '</span>';
|
||||
footerHtml += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
|
||||
footerHtml += '</div>';
|
||||
footerHtml += '</div>';
|
||||
|
||||
var $newFooter = $(footerHtml);
|
||||
this.$previewList.after($newFooter);
|
||||
|
||||
// Rebind load more click
|
||||
var self = this;
|
||||
if (this.previewOnLoadMore) {
|
||||
$newFooter.find('.btn-load-more').on('click', function() {
|
||||
var $btn = $(this);
|
||||
var $controls = $btn.closest('.load-more-controls');
|
||||
var $select = $controls.find('.load-more-select');
|
||||
|
||||
if ($btn.hasClass('loading')) return;
|
||||
|
||||
$btn.addClass('loading');
|
||||
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
|
||||
$select.prop('disabled', true);
|
||||
|
||||
var loadCount = parseInt($select.val(), 10) || 20;
|
||||
self.previewLoadCount = loadCount;
|
||||
|
||||
self.previewOnLoadMore.call(self, $btn);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$footer.remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
@@ -364,6 +513,10 @@
|
||||
this.$previewList = null;
|
||||
this.previewContext = null;
|
||||
this.previewOnLoadMore = null;
|
||||
this.previewOnFilter = null;
|
||||
this.previewCurrentFilter = '';
|
||||
this.previewEntityLabel = null;
|
||||
this.previewLockedWidth = null;
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
@@ -401,6 +554,57 @@
|
||||
context: { $tab: $tab, blockType: blockType },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreTabPreviewItems($tab, $btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterTabPreviewItems($tab, query);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* AJAX filter handler for tab preview
|
||||
*/
|
||||
filterTabPreviewItems: function($tab, query) {
|
||||
var self = this;
|
||||
var blockType = this.previewBlockType;
|
||||
|
||||
var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
|
||||
var savedData = {};
|
||||
try {
|
||||
savedData = JSON.parse($hiddenInput.val() || '{}');
|
||||
} catch (e) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
|
||||
if (groups.length === 0) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data[blockType] = { groups: groups };
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'previewTargetConditions',
|
||||
trait: 'TargetConditions',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
filter: query,
|
||||
limit: 20,
|
||||
offset: 0
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -425,19 +629,25 @@
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewTargetConditions',
|
||||
trait: 'TargetConditions',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
limit: self.previewLoadedCount + loadCount,
|
||||
offset: 0
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'previewTargetConditions',
|
||||
trait: 'TargetConditions',
|
||||
conditions: JSON.stringify(data),
|
||||
block_type: blockType,
|
||||
limit: self.previewLoadedCount + loadCount,
|
||||
offset: 0
|
||||
},
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
var items = response.items || response.products || [];
|
||||
if (response.success && items.length > 0) {
|
||||
@@ -504,6 +714,9 @@
|
||||
context: { conditionData: conditionData, blockType: blockType },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreConditionItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterConditionItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -518,13 +731,17 @@
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreConditionItems: function($btn) {
|
||||
/**
|
||||
* AJAX filter handler for condition preview
|
||||
*/
|
||||
filterConditionItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.conditionData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
if (!ctx || !ctx.conditionData) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -537,8 +754,45 @@
|
||||
method: ctx.conditionData.method,
|
||||
values: JSON.stringify(ctx.conditionData.values),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
filter: query,
|
||||
limit: 20
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreConditionItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.conditionData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewConditionItems',
|
||||
trait: 'EntitySelector',
|
||||
method: ctx.conditionData.method,
|
||||
values: JSON.stringify(ctx.conditionData.values),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.previewTotalCount = response.count;
|
||||
@@ -615,6 +869,9 @@
|
||||
context: { groupData: groupData, blockType: blockType, $group: $group },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreGroupItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterGroupItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -629,13 +886,17 @@
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreGroupItems: function($btn) {
|
||||
/**
|
||||
* AJAX filter handler for group preview
|
||||
*/
|
||||
filterGroupItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
if (!ctx || !ctx.groupData) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -647,8 +908,44 @@
|
||||
trait: 'EntitySelector',
|
||||
group_data: JSON.stringify(ctx.groupData),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
filter: query,
|
||||
limit: 20
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreGroupItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupData) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewGroupItems',
|
||||
trait: 'EntitySelector',
|
||||
group_data: JSON.stringify(ctx.groupData),
|
||||
block_type: ctx.blockType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.previewTotalCount = response.count;
|
||||
@@ -703,6 +1000,9 @@
|
||||
context: { groupId: groupId, groupType: groupType, groupName: groupName },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreFilterGroupItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterFilterGroupItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -717,13 +1017,17 @@
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreFilterGroupItems: function($btn) {
|
||||
/**
|
||||
* AJAX filter handler for filter group preview
|
||||
*/
|
||||
filterFilterGroupItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupId) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
if (!ctx || !ctx.groupId) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -735,8 +1039,44 @@
|
||||
trait: 'EntitySelector',
|
||||
group_id: ctx.groupId,
|
||||
group_type: ctx.groupType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
filter: query,
|
||||
limit: 20
|
||||
},
|
||||
success: function(response) {
|
||||
self.updatePreviewPopoverFiltered(response);
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreFilterGroupItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.groupId) return;
|
||||
|
||||
var loadCount = this.previewLoadCount || 20;
|
||||
|
||||
// Include current filter in load more request
|
||||
var ajaxData = {
|
||||
ajax: 1,
|
||||
action: 'previewFilterGroupProducts',
|
||||
trait: 'EntitySelector',
|
||||
group_id: ctx.groupId,
|
||||
group_type: ctx.groupType,
|
||||
limit: self.previewLoadedCount + loadCount
|
||||
};
|
||||
if (self.previewCurrentFilter) {
|
||||
ajaxData.filter = self.previewCurrentFilter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.previewTotalCount = response.count;
|
||||
|
||||
@@ -22,6 +22,24 @@
|
||||
// Utility functions mixin
|
||||
window._EntitySelectorMixins.utils = {
|
||||
|
||||
/**
|
||||
* Debounce function - delays execution until after wait milliseconds
|
||||
* @param {Function} func - Function to debounce
|
||||
* @param {number} wait - Milliseconds to wait
|
||||
* @returns {Function} Debounced function
|
||||
*/
|
||||
debounce: function(func, wait) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(function() {
|
||||
func.apply(context, args);
|
||||
}, wait);
|
||||
};
|
||||
},
|
||||
|
||||
escapeHtml: function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
|
||||
@@ -84,20 +84,55 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $es-spacing-xs;
|
||||
position: relative;
|
||||
|
||||
// Loading overlay for AJAX filtering
|
||||
&.filtering {
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.filter-loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba($es-white, 0.7);
|
||||
z-index: 10;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: $es-primary;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter input section
|
||||
.preview-filter {
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
padding: $es-spacing-sm;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
|
||||
.preview-filter-input {
|
||||
@include input-base;
|
||||
width: 100%;
|
||||
padding: $es-spacing-sm;
|
||||
font-size: $es-font-size-sm;
|
||||
padding: $es-spacing-xs $es-spacing-sm;
|
||||
font-size: $es-font-size-xs;
|
||||
line-height: 1.4;
|
||||
color: $es-text-primary;
|
||||
background-color: $es-white;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-sm;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: $es-primary;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $es-text-muted;
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user