refactor: entity selector full overhaul — Mar 2026

- Unified _setBadgeCount for ALL badge updates
- target-conditions-trait → entity-selector-trait
- target-* → es-* class rename (20+ classes)
- SCSS recompiled: zero duplicate selectors
- CSS transitions replace jQuery slideDown/slideUp
- Serialize cache, method swap cache
- Badge: no-matches gray, consistent hover, no blending
- Inline condition count always visible
- Preview popover refreshes in-place on sort change
- Categories add chips immediately
- Entity type icons on chips
- Consistent info_outline icons via buildHelpIcon
- Method dropdown text clipping fix (line-height)
- mpr-input-compact on all inputs
- Dropdown padding fixed in SCSS source
- Chips wrapper: same container always
- Reusable helpers: _buildEmptyState, _buildSearchBoxHtml, _buildInfoTooltip
- Asset path: uses $this->module->getPathUri() not reflection
- Debug logs removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 18:05:43 +00:00
parent d2d4f96c5e
commit c6fd5cee13
38 changed files with 7670 additions and 8929 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -29,13 +29,15 @@
addSelection: function($picker, id, name, data) {
this.addSelectionNoUpdate($picker, id, name, data);
var $chips = $picker.find('.entity-chips');
this.updateChipsVisibility($chips);
if (this.config.mode !== 'single') {
var $chips = $picker.find('.entity-chips');
this.updateChipsVisibility($chips);
}
},
addSelectionNoUpdate: function($picker, id, name, data) {
var $chips = $picker.find('.entity-chips');
var $block = $picker.closest('.target-block');
var $block = $picker.closest('.es-block');
// Check for global single mode (only ONE item across ALL entity types)
var globalMode = this.config.mode || 'multi';
@@ -47,9 +49,6 @@
if (this.$dropdown) {
this.$dropdown.find('.dropdown-item.selected, .tree-item.selected').removeClass('selected');
}
// Clear tab badges (since we're clearing other blocks)
this.$wrapper.find('.target-block-tab .tab-badge').remove();
this.$wrapper.find('.target-block-tab').removeClass('has-data');
} else {
// Check if this block is in per-block single mode
var blockMode = $block.data('mode') || 'multi';
@@ -68,6 +67,10 @@
return;
}
// Remove empty state placeholder
var hadEmpty = $chips.find('.chips-empty-state').length > 0;
$chips.find('.chips-empty-state').remove();
// Check if this is a country entity (for flag and holiday preview)
var blockType = $block.data('blockType') || '';
var searchEntity = $picker.attr('data-search-entity') || blockType;
@@ -79,21 +82,34 @@
}
html += '>';
// Country: show flag
// Icon: flag for countries, image if available, or entity type icon
if (isCountry && data && data.iso_code) {
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + this.escapeAttr(data.iso_code.toLowerCase()) + '.png" alt="' + this.escapeAttr(data.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';">' + this.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '</span>';
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + this.escapeAttr(data.iso_code.toLowerCase()) + '.png" alt="' + this.escapeAttr(data.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><i class="icon-flag flag-fallback" style="display:none;"></i></span>';
} else if (data && data.image) {
html += '<span class="chip-icon"><img src="' + this.escapeAttr(data.image) + '" alt=""></span>';
} else {
// Entity type icon from block config
var blockIcon = '';
if (this.config && this.config.blocks) {
var bt = $block.data('blockType') || '';
var blockDef = this.config.blocks[bt];
if (blockDef && blockDef.icon) {
blockIcon = blockDef.icon;
}
}
if (blockIcon) {
html += '<span class="chip-icon"><i class="' + this.escapeAttr(blockIcon) + '"></i></span>';
}
}
html += '<span class="chip-name">' + this.escapeHtml(name) + '</span>';
// Country: add holiday preview button
if (isCountry) {
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays">' + this.esIcon('visibility') + '</button>';
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
}
html += '<button type="button" class="chip-remove" title="Remove">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '</span>';
$chips.append(html);
@@ -112,19 +128,25 @@
var $allChips = $chips.find('.entity-chip');
var totalCount = $allChips.length;
// If no chips, remove the wrapper entirely
var $existingWrapper = $chips.closest('.chips-wrapper');
// Ensure wrapper always exists
this.ensureChipsWrapper($chips);
if (totalCount === 0) {
if ($existingWrapper.length) {
// Move chips out of wrapper before removing
$existingWrapper.before($chips);
$existingWrapper.remove();
// Show empty state, hide toolbar
var placeholder = $chips.data('placeholder') || trans.no_items_selected || 'No items selected';
if (!$chips.find('.chips-empty-state').length) {
$chips.html('<span class="chips-empty-state">' + self.escapeHtml(placeholder) + '</span>');
}
var $wrapper = $chips.closest('.chips-wrapper');
if ($wrapper.length) {
$wrapper.find('.chips-toolbar').removeClass('has-chips').hide();
$wrapper.find('.chips-load-more').hide();
}
return;
}
// Ensure chips wrapper structure exists
this.ensureChipsWrapper($chips);
// Has chips — show toolbar, remove empty state
$chips.find('.chips-empty-state').remove();
var $wrapper = $chips.closest('.chips-wrapper');
var $toolbar = $wrapper.find('.chips-toolbar');
@@ -160,7 +182,7 @@
});
// Update toolbar (always show when we have chips)
$toolbar.addClass('has-chips');
$toolbar.addClass('has-chips').show();
this.updateChipsToolbar($toolbar, totalCount, filteredCount, searchTerm);
// Update load more select dropdown
@@ -183,7 +205,7 @@
'<button type="button" class="btn-collapse-chips" style="' +
'background:transparent;border:1px solid #dee2e6;border-radius:4px;' +
'padding:0.25rem 0.75rem;font-size:12px;color:#6c757d;cursor:pointer;">' +
this.esIcon('expand_less') + ' ' + collapseText +
'<i class="icon-chevron-up"></i> ' + collapseText +
'</button>'
).show();
} else {
@@ -192,37 +214,44 @@
},
ensureChipsWrapper: function($chips) {
// Check if already wrapped
if ($chips.closest('.chips-wrapper').length) {
// Always create wrapper if missing
var $wrapper = $chips.closest('.chips-wrapper');
if (!$wrapper.length) {
$chips.wrap('<div class="chips-wrapper"></div>');
$wrapper = $chips.closest('.chips-wrapper');
$wrapper.prepend('<div class="chips-toolbar" style="display:none;"></div>');
$wrapper.append('<div class="chips-load-more" style="display:none;"></div>');
}
// Skip toolbar POPULATION for single mode
if (this.config.mode === 'single') {
return;
}
var $block = $chips.closest('.es-block');
if ($block.data('mode') === 'single') {
return;
}
var trans = this.config.trans || {};
var $picker = $chips.closest('.value-picker');
// If toolbar already populated, nothing to do
var $toolbar = $wrapper.find('.chips-toolbar');
if ($toolbar.children().length) {
return;
}
// Create wrapper structure - integrated filter toolbar with sort
var wrapperHtml = '<div class="chips-wrapper">' +
'<div class="chips-toolbar">' +
'<input type="text" class="chips-search-input" placeholder="' + (trans.filter_selected || 'Filter selected') + '...">' +
'<select class="chips-sort-select" title="' + (trans.sort || 'Sort') + '">' +
// Populate toolbar content
var trans = this.config.trans || {};
var toolbarHtml =
'<input type="text" class="chips-search-input mpr-input-compact" placeholder="' + (trans.filter_selected || 'Filter selected') + '...">' +
'<select class="chips-sort-select mpr-input-compact" title="' + (trans.sort || 'Sort') + '">' +
'<option value="added">' + (trans.sort_added || 'Order added') + '</option>' +
'<option value="name_asc">' + (trans.sort_name_asc || 'Name A-Z') + '</option>' +
'<option value="name_desc">' + (trans.sort_name_desc || 'Name Z-A') + '</option>' +
'</select>' +
'<span class="chips-count"></span>' +
'<button type="button" class="btn-chips-clear" title="' + (trans.clear_all || 'Clear all') + '">' +
this.esIcon('delete') + ' <span class="clear-text">' + (trans.clear || 'Clear') + '</span>' +
'</button>' +
'</div>' +
'<div class="chips-load-more" style="display:none;"></div>' +
'</div>';
var $wrapper = $(wrapperHtml);
// Insert wrapper before chips and move chips inside
$chips.before($wrapper);
$wrapper.find('.chips-toolbar').after($chips);
$wrapper.append($wrapper.find('.chips-load-more'));
'<i class="icon-trash"></i> <span class="clear-text">' + (trans.clear || 'Clear') + '</span>' +
'</button>';
$toolbar.html(toolbarHtml);
// Bind toolbar events
this.bindChipsToolbarEvents($wrapper);
@@ -361,16 +390,13 @@
loadExistingSelections: function() {
var self = this;
console.log('[EntitySelector] loadExistingSelections called for id:', this.config.id);
// Collect all entity IDs to load, grouped by entity type
var entitiesToLoad = {}; // { entity_type: { ids: [], pickers: [] } }
console.log('[EntitySelector] Looking for .selection-group in wrapper:', this.$wrapper.length ? 'found' : 'NOT FOUND');
this.$wrapper.find('.selection-group').each(function() {
console.log('[EntitySelector] Found .selection-group, index:', $(this).data('groupIndex'));
var $group = $(this);
var $block = $group.closest('.target-block');
var $block = $group.closest('.es-block');
var blockType = $block.data('blockType');
// Load include values
@@ -415,11 +441,9 @@
// Skip AJAX if no entities to load
if (!hasEntities) {
console.log('[EntitySelector] No entities to load, skipping AJAX');
return;
}
console.log('[EntitySelector] Making bulk AJAX request for entities:', JSON.stringify(bulkRequest));
// Single bulk AJAX call for all entity types
$.ajax({
@@ -433,9 +457,7 @@
entities: JSON.stringify(bulkRequest)
},
success: function(response) {
console.log('[EntitySelector] AJAX response:', response);
if (!response.success || !response.entities) {
console.log('[EntitySelector] Response failed or no entities');
return;
}
try {
@@ -475,21 +497,27 @@
}
html += '>';
// Country: show flag
// Icon: flag, image, or entity type icon
if (isCountry && entity.iso_code) {
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + self.escapeAttr(entity.iso_code.toLowerCase()) + '.png" alt="' + self.escapeAttr(entity.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';">' + self.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '</span>';
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + self.escapeAttr(entity.iso_code.toLowerCase()) + '.png" alt="' + self.escapeAttr(entity.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><i class="icon-flag flag-fallback" style="display:none;"></i></span>';
} else if (entity.image) {
html += '<span class="chip-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
} else {
var bt = $block.data('blockType') || '';
var blockDef = self.config.blocks && self.config.blocks[bt];
if (blockDef && blockDef.icon) {
html += '<span class="chip-icon"><i class="' + self.escapeAttr(blockDef.icon) + '"></i></span>';
}
}
html += '<span class="chip-name">' + self.escapeHtml(entity.name) + '</span>';
// Country: add holiday preview button
if (isCountry) {
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays">' + self.esIcon('visibility') + '</button>';
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
}
html += '<button type="button" class="chip-remove" title="Remove">' + self.esIcon('close') + '</button>';
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '</span>';
$loadingChip.replaceWith(html);
@@ -508,7 +536,7 @@
self.serializeAllBlocks();
}
self.updateBlockStatus($picker.closest('.target-block'));
self.updateBlockStatus($picker.closest('.es-block'));
});
});
@@ -530,30 +558,23 @@
* Also shows loading placeholders for entity_search types
*/
collectPickerEntities: function($picker, blockType, entitiesToLoad) {
console.log('[EntitySelector] collectPickerEntities called, blockType:', blockType, 'picker length:', $picker.length);
if (!$picker.length) {
console.log('[EntitySelector] Picker not found, returning');
return;
}
var self = this;
var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
console.log('[EntitySelector] Looking for values-data input, found:', $dataInput.length);
if (!$dataInput.length) {
console.log('[EntitySelector] No data input found, returning');
return;
}
var valueType = $picker.attr('data-value-type');
var rawValue = $dataInput.val() || '[]';
console.log('[EntitySelector] valueType:', valueType, 'rawValue:', rawValue);
var values = [];
try {
values = JSON.parse(rawValue);
console.log('[EntitySelector] Parsed values:', values);
} catch (e) {
console.log('[EntitySelector] JSON parse error:', e);
return;
}
@@ -583,7 +604,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: self.esIcon('close')
html: '<i class="icon-times"></i>'
}));
$chipsContainer.append($chip);
@@ -636,7 +657,7 @@
// Show loading placeholders with entity-specific icons
values.forEach(function(id) {
var html = '<span class="entity-chip entity-chip-loading" data-id="' + self.escapeAttr(id) + '">';
html += '<span class="chip-icon">' + self.esIcon(entityIcon, 'es-spin-pulse') + '</span>';
html += '<span class="chip-icon"><i class="' + entityIcon + ' icon-spin-pulse"></i></span>';
html += '<span class="chip-name">Loading...</span>';
html += '</span>';
$chips.append(html);
@@ -697,7 +718,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: self.esIcon('close')
html: '<i class="icon-times"></i>'
}));
$chipsContainer.append($chip);
@@ -783,7 +804,7 @@
self.serializeAllBlocks();
}
self.updateBlockStatus($picker.closest('.target-block'));
self.updateBlockStatus($picker.closest('.es-block'));
}
}
});
@@ -810,7 +831,7 @@
html += '<span class="case-icon">' + (isCaseSensitive ? 'Aa' : 'aa') + '</span>';
html += '</button>';
html += '<span class="pattern-tag-text">' + this.escapeHtml(pattern) + '</span>';
html += '<button type="button" class="btn-remove-pattern" title="' + this.escapeAttr(trans.remove_pattern || 'Remove pattern') + '">' + this.esIcon('delete') + '</button>';
html += '<button type="button" class="btn-remove-pattern" title="' + this.escapeAttr(trans.remove_pattern || 'Remove pattern') + '"><i class="icon-trash"></i></button>';
html += '</div>';
$chipsContainer.append(html);
},
@@ -845,12 +866,11 @@
var $countValue = $matchCount.find('.count-value');
// Get entity type from block
var $block = $draftTag.closest('.target-block');
var $block = $draftTag.closest('.es-block');
var entityType = $block.data('blockType') || 'products';
// Show loading - keep eye icon, update count value
$countValue.html(this.esIcon('progress_activity', 'es-spin'));
$matchCount.show();
// Show loading state — keep existing count visible
if ($matchCount.hasClass('no-matches')) { $matchCount.addClass('loading-count'); } $matchCount.show();
// Store pattern for click handler
$matchCount.data('pattern', pattern);
@@ -916,7 +936,7 @@
var method = $methodSelect.val();
if (!method) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return;
}
@@ -940,16 +960,15 @@
}
if (values.length === 0) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return;
}
var $block = $row.closest('.target-block');
var $block = $row.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
// Show loading
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Show loading state — keep existing count visible
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
// Store condition data on badge for popover
$countEl.data('conditionData', {
@@ -974,20 +993,13 @@
success: function(response) {
if (response && response.success) {
var count = response.count || 0;
$countEl.removeClass('no-matches clickable');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
}
self._setBadgeCount($countEl, count);
} else {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
},
@@ -996,6 +1008,7 @@
* Fetch pattern match count via AJAX
*/
fetchPatternMatchCount: function($picker, pattern, $countEl) {
var self = this;
// Determine field type from method select
// Check if we're in an exclude row first, then fall back to include
var $excludeRow = $picker.closest('.exclude-row');
@@ -1010,12 +1023,12 @@
var field = method.indexOf('reference') !== -1 ? 'reference' : 'name';
// Get entity type from block
var $block = $picker.closest('.target-block');
var $block = $picker.closest('.es-block');
var entityType = $block.data('blockType') || 'products';
// Show loading state
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
$countEl.html('<i class="icon-spinner icon-spin"></i>');
$countEl.removeClass('clickable no-matches').addClass('loading-count').show();
$.ajax({
url: this.config.ajaxUrl,
@@ -1033,19 +1046,13 @@
success: function(response) {
if (response && response.success) {
var count = response.count || 0;
$countEl.find('.preview-count').text(count);
$countEl.removeClass('no-matches clickable').show();
if (count === 0) {
$countEl.addClass('no-matches');
} else {
$countEl.addClass('clickable');
}
self._setBadgeCount($countEl, count);
} else {
$countEl.hide();
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide();
self._setBadgeCount($countEl, 0);
}
});
},
@@ -1125,7 +1132,7 @@
var method = $methodSelect.val();
if (!method) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return;
}
@@ -1147,17 +1154,16 @@
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
));
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return;
}
// Get block type
var $block = $row.closest('.target-block');
var $block = $row.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
// Show loading
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Show loading state — keep existing count visible
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
// Store condition data on badge for popover
$countEl.data('conditionData', {
@@ -1182,21 +1188,13 @@
success: function(response) {
if (response && response.success) {
var count = response.count || 0;
$countEl.removeClass('no-matches clickable');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
} else {
// Show count, make clickable for preview popover
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
}
self._setBadgeCount($countEl, count);
} else {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
},
@@ -1228,7 +1226,7 @@
*/
updateGroupTotalCount: function($group) {
var self = this;
var $block = $group.closest('.target-block');
var $block = $group.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
var $badge = $group.find('.group-header .group-count-badge');
var $limitInput = $group.find('.group-modifier-limit');
@@ -1244,7 +1242,7 @@
}
// Show loading
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
$badge.html('<i class="icon-spinner icon-spin"></i>').show();
$.ajax({
url: this.config.ajaxUrl,
@@ -1262,13 +1260,15 @@
var finalCount = response.final_count || 0;
var excludeCount = response.exclude_count || 0;
// Update badge with eye icon and count
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
// Update badge with eye icon and count (custom HTML for exclude info)
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
if (excludeCount > 0) {
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
}
$badge.html(badgeHtml);
$badge.addClass('clickable').show();
if ($badge.html() !== badgeHtml) {
$badge.html(badgeHtml);
}
self._setBadgeCount($badge, finalCount);
// Store group data on badge for preview popover
$badge.data('groupData', groupData);
@@ -1286,12 +1286,12 @@
$previewBadge.text(displayCount);
}
} else {
$badge.hide().removeClass('clickable');
self._setBadgeCount($badge, 0);
$limitInput.attr('placeholder', '');
}
},
error: function() {
$badge.hide();
self._setBadgeCount($badge, 0);
$limitInput.attr('placeholder', '');
}
});
@@ -1302,7 +1302,7 @@
*/
updateAllConditionCounts: function() {
var self = this;
this.$wrapper.find('.target-block.active .selection-group').each(function() {
this.$wrapper.find('.es-block.active .selection-group').each(function() {
self.updateGroupCounts($(this));
});
},

View File

@@ -102,7 +102,7 @@
init: function(options) {
this.config = $.extend({
id: 'target-conditions',
id: 'entity-selector',
name: 'target_conditions',
namePrefix: 'target_',
mode: 'multi', // Global mode: 'multi' or 'single'
@@ -117,7 +117,7 @@
return;
}
// Global single mode - hide "Add Group" buttons
// Global single mode - hide group management
if (this.config.mode === 'single') {
this.$wrapper.find('.btn-add-group').hide();
this.$wrapper.find('.group-excludes').hide();
@@ -132,6 +132,7 @@
var $formGroup = this.$wrapper.closest('.form-group');
$formGroup.addClass('condition-trait-fullwidth');
$formGroup.find('.col-lg-offset-3').removeClass('col-lg-offset-3');
} else {
}
this.createDropdown();
@@ -147,10 +148,14 @@
// Update counts on page load
var self = this;
setTimeout(function() {
self.updateTabBadges();
self.updateAllConditionCounts();
}, 100);
if (this.config.mode === 'single') {
setTimeout(function() { self._updateSingleModeBadges(); }, 100);
} else {
setTimeout(function() {
self.updateTabBadges();
self.updateAllConditionCounts();
}, 100);
}
},
observeNewSelects: function() {
@@ -287,15 +292,15 @@
});
// Tips box toggle handler
$(document).on('click', '.target-tips-box .tips-header', function(e) {
$(document).on('click', '.es-tips-box .tips-header', function(e) {
e.preventDefault();
$(this).closest('.target-tips-box').toggleClass('expanded');
$(this).closest('.es-tips-box').toggleClass('expanded');
});
// Form submission validation for required target conditions
$(document).on('submit', 'form', function(e) {
var $form = $(this);
if ($form.find('.target-conditions-trait[data-required]').length > 0) {
if ($form.find('.entity-selector-trait[data-required]').length > 0) {
if (!TargetConditions.validateAll()) {
e.preventDefault();
return false;

View File

@@ -12,11 +12,11 @@
window._EntitySelectorMixins.dropdown = {
createDropdown: function() {
this.$wrapper.find('.target-search-dropdown').remove();
this.$wrapper.find('.es-search-dropdown').remove();
var trans = this.config.trans || {};
var html = '<div class="target-search-dropdown view-list">';
var html = '<div class="es-search-dropdown view-list">';
// Header with results count, actions, sort controls, view mode
html += '<div class="dropdown-header">';
@@ -26,10 +26,10 @@
// Select all / Clear buttons with keyboard shortcuts
html += '<button type="button" class="btn-select-all" title="' + (trans.select_all || 'Select all visible') + '">';
html += this.esIcon('check_box') + ' ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += '<i class="icon-check-square-o"></i> ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += '</button>';
html += '<button type="button" class="btn-clear-selection" title="' + (trans.clear_selection || 'Clear selection') + '">';
html += this.esIcon('check_box_outline_blank') + ' ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += '<i class="icon-square-o"></i> ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += '</button>';
// Sort controls - options with data-entities attribute for entity-specific filtering
@@ -50,7 +50,7 @@
html += '<option value="product_count" data-entities="categories,manufacturers,suppliers">' + (trans.sort_product_count || 'Products') + '</option>';
html += '</select>';
html += '<button type="button" class="btn-sort-dir" data-dir="ASC" title="Sort direction">';
html += this.esIcon('sort_by_alpha');
html += '<i class="icon-sort-alpha-asc"></i>';
html += '</button>';
// View mode selector - Tree option always present, shown for categories
@@ -69,19 +69,19 @@
// Refine search
html += '<div class="refine-compact">';
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '">' + this.esIcon('block') + '</button>';
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '"><i class="icon-ban"></i></button>';
html += '<input type="text" class="refine-input" placeholder="' + (trans.refine_short || 'Refine...') + '">';
html += '<button type="button" class="btn-clear-refine" style="display:none;">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-refine" style="display:none;"><i class="icon-times"></i></button>';
html += '</div>';
// Filter toggle button
html += '<button type="button" class="btn-toggle-filters" title="' + (trans.toggle_filters || 'Filters') + '">';
html += this.esIcon('filter_list');
html += '<i class="icon-filter"></i>';
html += '</button>';
// History button
html += '<button type="button" class="btn-show-history" title="' + (trans.recent_searches || 'Recent searches') + '">';
html += this.esIcon('schedule');
html += '<i class="icon-clock-o"></i>';
html += '</button>';
html += '</div>'; // End dropdown-actions
@@ -104,13 +104,13 @@
html += '</div>';
html += '<button type="button" class="btn-clear-filters" title="' + (trans.clear_filters || 'Clear filters') + '">';
html += this.esIcon('close');
html += '<i class="icon-times"></i>';
html += '</button>';
html += '</div>';
// Attribute/Feature filter toggles for products
html += '<div class="filter-row filter-row-attributes" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label">' + this.esIcon('label') + ' ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<span class="filter-row-label"><i class="icon-tags"></i> ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<div class="filter-attributes-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-attr-values" data-type="attribute" style="display:none;">';
@@ -118,7 +118,7 @@
html += '</div>';
html += '<div class="filter-row filter-row-features" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label">' + this.esIcon('list') + ' ' + (trans.features || 'Features') + ':</span>';
html += '<span class="filter-row-label"><i class="icon-list-ul"></i> ' + (trans.features || 'Features') + ':</span>';
html += '<div class="filter-features-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-feat-values" data-type="feature" style="display:none;">';
@@ -129,19 +129,19 @@
html += '<div class="filter-row filter-row-entity-categories filter-row-multi" data-entity="categories" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -150,7 +150,7 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('account_tree') + ' ' + (trans.depth || 'Depth') + ':</span>';
html += '<span class="filter-select-label"><i class="icon-sitemap"></i> ' + (trans.depth || 'Depth') + ':</span>';
html += '<select class="filter-depth-select">';
html += '<option value="">' + (trans.all_levels || 'All levels') + '</option>';
html += '<option value="1">' + (trans.level || 'Level') + ' 1 (' + (trans.root || 'Root') + ')</option>';
@@ -162,7 +162,7 @@
html += '<label class="filter-label"><input type="checkbox" class="filter-has-products"> ' + (trans.has_products || 'Has products') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-description"> ' + (trans.has_description || 'Has description') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-image"> ' + (trans.has_image || 'Has image') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
html += '</div>';
@@ -170,19 +170,19 @@
html += '<div class="filter-row filter-row-entity-manufacturers filter-row-multi" data-entity="manufacturers" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -191,18 +191,18 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
html += '<span class="filter-date-label"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
html += '</div>';
@@ -210,19 +210,19 @@
html += '<div class="filter-row filter-row-entity-suppliers filter-row-multi" data-entity="suppliers" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -231,18 +231,18 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
html += '<span class="filter-date-label"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
html += '</div>';
@@ -250,19 +250,19 @@
html += '<div class="filter-row filter-row-entity-attributes filter-row-multi" data-entity="attributes" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -270,13 +270,13 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('label') + ' ' + (trans.attribute_group || 'Group') + ':</span>';
html += '<span class="filter-select-label"><i class="icon-tags"></i> ' + (trans.attribute_group || 'Group') + ':</span>';
html += '<select class="filter-attribute-group-select">';
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-is-color"> ' + (trans.color_only || 'Color attributes') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
html += '</div>';
@@ -284,19 +284,19 @@
html += '<div class="filter-row filter-row-entity-features filter-row-multi" data-entity="features" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -304,13 +304,13 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('list') + ' ' + (trans.feature_group || 'Group') + ':</span>';
html += '<span class="filter-select-label"><i class="icon-list-ul"></i> ' + (trans.feature_group || 'Group') + ':</span>';
html += '<select class="filter-feature-group-select">';
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-is-custom"> ' + (trans.custom_only || 'Custom values') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
html += '</div>';
@@ -318,13 +318,13 @@
html += '<div class="filter-row filter-row-entity-cms" data-entity="cms" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-indexable"> ' + (trans.indexable || 'Indexable') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
// Entity-specific filters: CMS Categories
html += '<div class="filter-row filter-row-entity-cms-categories" data-entity="cms_categories" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
// Entity-specific filters: Countries
@@ -333,12 +333,12 @@
html += '<label class="filter-label"><input type="checkbox" class="filter-has-holidays"> ' + (trans.has_holidays || 'Has holidays') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-contains-states"> ' + (trans.contains_states || 'Has states') + '</label>';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('language') + ' ' + (trans.zone || 'Zone') + ':</span>';
html += '<span class="filter-select-label"><i class="icon-globe"></i> ' + (trans.zone || 'Zone') + ':</span>';
html += '<select class="filter-zone-select">';
html += '<option value="">' + (trans.all_zones || 'All zones') + '</option>';
html += '</select>';
html += '</div>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '</div>';
html += '</div>'; // End filter-panel
@@ -373,8 +373,8 @@
// Right side: action buttons
html += '<div class="dropdown-footer-right">';
html += '<button type="button" class="dropdown-action-btn btn-cancel">' + this.esIcon('close') + ' ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-save">' + this.esIcon('check') + ' ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-cancel"><i class="icon-times"></i> ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-save"><i class="icon-check"></i> ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
html += '</div>';
html += '</div>';

View File

@@ -35,18 +35,18 @@
var self = this;
// Tab switching
this.$wrapper.on('click', '.target-block-tab', function(e) {
this.$wrapper.on('click', '.es-block-tab', function(e) {
e.preventDefault();
var blockType = $(this).data('blockType');
self.switchToBlock(blockType);
});
// Tab badge click for preview popover (toggle)
this.$wrapper.on('click', '.target-block-tab .tab-badge', function(e) {
this.$wrapper.on('click', '.es-block-tab .tab-badge', function(e) {
e.stopPropagation();
e.preventDefault();
var $tab = $(this).closest('.target-block-tab');
var $tab = $(this).closest('.es-block-tab');
var $badge = $(this);
if ($badge.hasClass('popover-open')) {
@@ -100,7 +100,7 @@
// Close popover when clicking outside
$(document).on('click', function(e) {
if (!$(e.target).closest('.target-preview-popover').length &&
if (!$(e.target).closest('.es-preview-popover').length &&
!$(e.target).closest('.holiday-preview-popover').length &&
!$(e.target).closest('.tab-badge').length &&
!$(e.target).closest('.condition-match-count').length &&
@@ -118,19 +118,18 @@
// Block-level collapse toggle (click on header)
this.$wrapper.on('click', '.condition-trait-header', function(e) {
if ($(e.target).closest('.target-block-tabs').length ||
if ($(e.target).closest('.es-block-tabs').length ||
$(e.target).closest('.trait-header-actions').length ||
$(e.target).closest('.prestashop-switch').length ||
$(e.target).closest('.trait-total-count').length) {
return;
}
var $body = self.$wrapper.find('.condition-trait-body');
$body.stop(true, true);
if ($body.is(':visible')) {
$body.slideUp(200);
if ($body.hasClass('es-expanded')) {
$body.removeClass('es-expanded');
self.$wrapper.addClass('collapsed');
} else {
$body.slideDown(200);
$body.addClass('es-expanded');
self.$wrapper.removeClass('collapsed');
}
});
@@ -139,14 +138,13 @@
this.$wrapper.on('click', '.btn-toggle-blocks', function(e) {
e.preventDefault();
var $blocksContent = self.$wrapper.find('.entity-selector-blocks-content');
var $icon = $(this).find('.es-icon');
$blocksContent.stop(true, true);
if ($blocksContent.is(':visible')) {
$blocksContent.slideUp(200);
var $icon = $(this).find('.material-icons');
if ($blocksContent.hasClass('es-expanded')) {
$blocksContent.removeClass('es-expanded');
self.$wrapper.addClass('blocks-collapsed');
$icon.text('expand_more');
} else {
$blocksContent.slideDown(200);
$blocksContent.addClass('es-expanded');
self.$wrapper.removeClass('blocks-collapsed');
$icon.text('expand_less');
}
@@ -170,27 +168,23 @@
});
// Toggle all groups (single button that switches between expand/collapse)
console.log('[ES-DEBUG] Binding .btn-toggle-groups click on wrapper:', self.$wrapper.attr('id'), 'found buttons:', self.$wrapper.find('.btn-toggle-groups').length);
this.$wrapper.on('click', '.btn-toggle-groups', function(e) {
this.$wrapper.on('click', '.trait-header-actions .btn-toggle-groups', function(e) {
e.preventDefault();
e.stopPropagation();
var $btn = $(this);
var currentState = $btn.attr('data-state') || 'collapsed';
var trans = self.config.trans || {};
console.log('[ES-DEBUG] .btn-toggle-groups CLICKED! currentState:', currentState, 'btn parent:', $btn.parent().attr('class'), 'groups found:', self.$wrapper.find('.selection-group').length);
if (currentState === 'collapsed') {
self.$wrapper.find('.selection-group').removeClass('collapsed');
$btn.attr('data-state', 'expanded');
$btn.attr('title', trans.collapse_all || 'Collapse all groups');
$btn.find('i').text('close_fullscreen');
console.log('[ES-DEBUG] Expanded all groups');
$btn.find('i').removeClass('icon-expand').addClass('icon-compress');
} else {
self.$wrapper.find('.selection-group').addClass('collapsed');
$btn.attr('data-state', 'collapsed');
$btn.attr('title', trans.expand_all || 'Expand all groups');
$btn.find('i').text('open_in_full');
console.log('[ES-DEBUG] Collapsed all groups');
$btn.find('i').removeClass('icon-compress').addClass('icon-expand');
}
});
@@ -204,15 +198,15 @@
});
// Target switch change (PrestaShop native switch)
this.$wrapper.on('change', '.target-switch-toggle', function(e) {
this.$wrapper.on('change', '.es-switch-toggle', function(e) {
e.stopPropagation();
var value = $(this).val();
if (value === '1') {
self.clearAllConditions();
self.$wrapper.find('.condition-trait-body').slideUp(200);
self.$wrapper.find('.condition-trait-body').removeClass('es-expanded');
self.$wrapper.addClass('collapsed');
} else {
self.$wrapper.find('.condition-trait-body').slideDown(200);
self.$wrapper.find('.condition-trait-body').addClass('es-expanded');
self.$wrapper.removeClass('collapsed');
}
});
@@ -220,7 +214,7 @@
// Add group
this.$wrapper.on('click', '.btn-add-group', function(e) {
e.preventDefault();
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
var blockType = $block.data('blockType');
self.addGroup($block, blockType);
});
@@ -229,7 +223,7 @@
this.$wrapper.on('click', '.btn-remove-group', function(e) {
e.preventDefault();
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
self.removeGroup($group, $block);
});
@@ -251,7 +245,7 @@
this.$wrapper.on('click', '.btn-add-exclude', function(e) {
e.preventDefault();
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
self.addFirstExcludeRow($group, $block);
});
@@ -259,7 +253,7 @@
this.$wrapper.on('click', '.btn-add-another-exclude', function(e) {
e.preventDefault();
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
self.addExcludeRow($group, $block);
});
@@ -268,16 +262,21 @@
e.preventDefault();
var $excludeRow = $(this).closest('.exclude-row');
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
self.removeExcludeRow($excludeRow, $group, $block);
});
// Include method change
this.$wrapper.on('change', '.include-method-select', function() {
var $group = $(this).closest('.selection-group');
var newMethod = $(this).val();
var prevMethod = $group.data('_currentIncludeMethod');
if (prevMethod === newMethod) return;
$group.data('_currentIncludeMethod', newMethod);
self.hideDropdown();
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
var $row = $group.find('.group-include');
var blockType = $block.data('blockType');
var blockDef = self.config.blocks[blockType] || {};
@@ -328,11 +327,16 @@
// Exclude method change (within an exclude row)
this.$wrapper.on('change', '.exclude-method-select', function() {
var $excludeRow = $(this).closest('.exclude-row');
var newMethod = $(this).val();
var prevMethod = $excludeRow.data('_currentExcludeMethod');
if (prevMethod === newMethod) return;
$excludeRow.data('_currentExcludeMethod', newMethod);
self.hideDropdown();
var $excludeRow = $(this).closest('.exclude-row');
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
var blockType = $block.data('blockType');
var blockDef = self.config.blocks[blockType] || {};
var methods = blockDef.selection_methods || {};
@@ -510,8 +514,8 @@
var currentPattern = $tag.data('pattern');
var $editInput = $('<input type="text" class="pattern-tag-edit">').val(currentPattern);
var $saveBtn = $('<button type="button" class="btn-pattern-save" title="Save">' + this.esIcon('check') + '</button>');
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel">' + this.esIcon('close') + '</button>');
var $saveBtn = $('<button type="button" class="btn-pattern-save" title="Save"><i class="icon-check"></i></button>');
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel"><i class="icon-times"></i></button>');
var $editActions = $('<span class="pattern-edit-actions"></span>').append($saveBtn, $cancelBtn);
$tag.addClass('editing').find('.pattern-tag-text').hide();
@@ -560,6 +564,50 @@
$tag.removeClass('editing').find('.pattern-tag-text, .btn-remove-pattern').show();
});
// Handle mpr-info-wrapper tooltip with fixed positioning
this.$wrapper.on('mouseenter', '.mpr-info-wrapper[data-details]', function() {
var $wrapper = $(this);
if ($wrapper.data('tooltip-active')) return;
var content = $wrapper.attr('data-details');
var tooltipClass = $wrapper.attr('data-tooltip-class') || '';
var $tooltip = $('<div>', { class: 'mpr-tooltip mpr-tooltip-fixed ' + tooltipClass, html: content });
$('body').append($tooltip);
$wrapper.data('tooltip-active', true);
var offset = $wrapper.offset();
var triggerWidth = $wrapper.outerWidth();
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var left = offset.left + (triggerWidth / 2) - (tooltipWidth / 2);
var top = offset.top - tooltipHeight - 10;
if (left < 10) left = 10;
if (left + tooltipWidth > $(window).width() - 10) {
left = $(window).width() - tooltipWidth - 10;
}
$tooltip.css({
position: 'fixed',
left: left + 'px',
top: (top - $(window).scrollTop()) + 'px'
});
$wrapper.data('tooltip-el', $tooltip);
});
this.$wrapper.on('mouseleave', '.mpr-info-wrapper[data-details]', function() {
var $wrapper = $(this);
var $tooltip = $wrapper.data('tooltip-el');
if ($tooltip) {
$tooltip.remove();
}
$wrapper.data('tooltip-active', false);
$wrapper.data('tooltip-el', null);
});
// Handle numeric range input changes
this.$wrapper.on('change', '.range-min-input, .range-max-input', function() {
var $row = $(this).closest('.group-include, .exclude-row');
@@ -655,7 +703,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: self.esIcon('close')
html: '<i class="icon-times"></i>'
}));
$chipsContainer.append($chip);
@@ -768,9 +816,8 @@
var $btn = $(this);
var $modifiers = $btn.closest('.group-modifiers');
var $content = $modifiers.find('.group-modifiers-content');
$content.slideToggle(150, function() {
$modifiers.toggleClass('expanded', $content.is(':visible'));
});
$content.toggleClass('es-expanded');
$modifiers.toggleClass('expanded', $content.hasClass('es-expanded'));
});
// Handle group-level modifier changes
@@ -811,7 +858,11 @@
$btn.attr('data-dir', newDir);
var $icon = $btn.find('i');
$icon.replaceWith(this.esIcon('sort'));
if (newDir === 'ASC') {
$icon.removeClass('icon-sort-amount-desc').addClass('icon-sort-amount-asc');
} else {
$icon.removeClass('icon-sort-amount-asc').addClass('icon-sort-amount-desc');
}
self.serializeAllBlocks();
self.refreshGroupPreviewIfOpen($group);
@@ -823,7 +874,7 @@
e.stopPropagation();
var $badge = $(this);
var $group = $badge.closest('.selection-group');
var $block = $badge.closest('.target-block');
var $block = $badge.closest('.es-block');
var blockType = $block.data('blockType');
if ($badge.hasClass('popover-open')) {
@@ -838,7 +889,7 @@
this.$wrapper.on('focus', '.entity-search-input', function() {
var $picker = $(this).closest('.value-picker');
var $group = $(this).closest('.selection-group');
var $block = $(this).closest('.target-block');
var $block = $(this).closest('.es-block');
var blockType = $block.data('blockType');
var groupIndex = parseInt($group.data('groupIndex'), 10);
var section = $picker.hasClass('include-picker') ? 'include' : 'exclude';
@@ -930,7 +981,7 @@
if (query && self.activeGroup) {
var $input = self.$wrapper.find('.entity-search-input:focus');
if (!$input.length) {
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
$input = $group.find('.entity-search-input').first();
}
@@ -972,7 +1023,7 @@
if (!self.activeGroup) return;
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
var $picker;
var $row;
@@ -1040,8 +1091,6 @@
var $row = $(this).closest('.group-include, .exclude-row');
var id = $chip.data('id');
console.log('[EntitySelector] Chip remove clicked, id:', id);
// Also remove from pending selections if dropdown is open
if (self.pendingSelections) {
self.pendingSelections = self.pendingSelections.filter(function(s) {
@@ -1050,7 +1099,6 @@
}
self.removeSelection($picker, id);
console.log('[EntitySelector] Calling serializeAllBlocks after chip remove');
self.serializeAllBlocks($row);
if (self.$dropdown && self.$dropdown.hasClass('show')) {
@@ -1140,7 +1188,7 @@
}
// Handle list view
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
var $picker;
var $row;
@@ -1219,7 +1267,7 @@
}
// Handle list view
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
var $picker;
var $row;
@@ -1306,7 +1354,7 @@
var currentDir = $btn.data('dir');
var newDir = currentDir === 'ASC' ? 'DESC' : 'ASC';
$btn.data('dir', newDir);
$btn.find('i').replaceWith(this.esIcon('sort_by_alpha'));
$btn.find('i').attr('class', newDir === 'ASC' ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc');
self.currentSort.dir = newDir;
self.refreshSearch();
});
@@ -1320,7 +1368,8 @@
$item.toggleClass('collapsed');
var isCollapsed = $item.hasClass('collapsed');
$(this).find('i').text(isCollapsed ? 'arrow_right' : 'arrow_drop_down');
$(this).find('i').toggleClass('icon-caret-down', !isCollapsed)
.toggleClass('icon-caret-right', isCollapsed);
var descendants = self.findTreeDescendants($item, $allItems);
for (var i = 0; i < descendants.length; i++) {
@@ -1359,11 +1408,19 @@
};
if (isSelected) {
// Remove from pending
// Remove from pending and remove chip
self.pendingSelections = self.pendingSelections.filter(function(s) {
return parseInt(s.id, 10) !== id;
});
$item.removeClass('selected');
// Remove chip immediately
var $picker = self.$wrapper.find('.es-block.active .value-picker.include-picker, .es-block.active .value-picker.exclude-picker').first();
if ($picker.length) {
$picker.find('.entity-chip[data-id="' + id + '"]').remove();
var $row = $picker.closest('.group-include, .exclude-row');
self.serializeAllBlocks($row);
}
} else {
// Validate selection before adding to pending
var section = self.activeGroup.section;
@@ -1373,7 +1430,7 @@
return;
}
// Add to pending
// Add to pending and create chip immediately
var exists = self.pendingSelections.some(function(s) {
return parseInt(s.id, 10) === id;
});
@@ -1385,6 +1442,14 @@
});
}
$item.addClass('selected');
// Immediately add chip (same as product list behavior)
var $picker = self.$wrapper.find('.es-block.active .value-picker.include-picker, .es-block.active .value-picker.exclude-picker').first();
if ($picker.length) {
self.addSelection($picker, id, name, $item.data());
var $row = $picker.closest('.group-include, .exclude-row');
self.serializeAllBlocks($row);
}
}
updateCount();
@@ -1416,7 +1481,7 @@
if (!self.activeGroup) return;
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
var $picker;
var $row;
@@ -1451,7 +1516,7 @@
$child.removeClass('selected');
}
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
$btn.attr('title', trans.select_with_children || 'Select with all children');
} else {
var section = self.activeGroup.section;
@@ -1489,7 +1554,7 @@
self.showValidationError(skipMsg);
}
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
}
@@ -1510,7 +1575,7 @@
this.$dropdown.on('click', '.category-tree .btn-expand-all', function(e) {
e.preventDefault();
self.$dropdown.find('.tree-item').removeClass('collapsed').show();
self.$dropdown.find('.tree-toggle i').replaceWith(this.esIcon('arrow_drop_down'));
self.$dropdown.find('.tree-toggle i').removeClass('icon-caret-right').addClass('icon-caret-down');
});
// Tree view: Collapse all
@@ -1530,7 +1595,7 @@
if (level === minLevel) {
if (hasChildren) {
$item.addClass('collapsed');
$item.find('.tree-toggle i').replaceWith(self.esIcon('arrow_right'));
$item.find('.tree-toggle i').removeClass('icon-caret-down').addClass('icon-caret-right');
}
$item.show();
} else {
@@ -1888,8 +1953,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-preview-popover').length) {
!$(e.target).closest('.es-search-dropdown').length &&
!$(e.target).closest('.es-preview-popover').length) {
self.hideDropdown();
}
});
@@ -1928,6 +1993,104 @@
}
});
// Tooltip hover events
this.$wrapper.on('mouseenter', '.mpr-info-wrapper:not(.pinned)', function() {
var $wrapper = $(this);
var content = $wrapper.attr('data-tooltip');
if (!content) return;
// Don't show hover tooltip if another is pinned
if ($('.mpr-tooltip-fixed.pinned').length) return;
// Remove any existing non-pinned tooltip
$('.mpr-tooltip-fixed:not(.pinned)').remove();
// Create tooltip
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed' }).html(content);
$('body').append($tooltip);
// Use getBoundingClientRect for viewport-relative positioning (fixed)
var rect = $wrapper[0].getBoundingClientRect();
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
var top = rect.top - tooltipHeight - 8;
// Keep tooltip within viewport
if (left < 10) left = 10;
if (left + tooltipWidth > window.innerWidth - 10) {
left = window.innerWidth - tooltipWidth - 10;
}
if (top < 10) {
top = rect.bottom + 8;
}
$tooltip.css({ top: top, left: left });
});
this.$wrapper.on('mouseleave', '.mpr-info-wrapper:not(.pinned)', function() {
$('.mpr-tooltip-fixed:not(.pinned)').remove();
});
// Click to pin tooltip
this.$wrapper.on('click', '.mpr-info-wrapper', function(e) {
e.preventDefault();
e.stopPropagation();
var $wrapper = $(this);
// If already pinned, unpin and close
if ($wrapper.hasClass('pinned')) {
$wrapper.removeClass('pinned');
$wrapper.find('.material-icons').text('info_outline');
$('.mpr-tooltip-fixed.pinned').remove();
return;
}
// Close any other pinned tooltips
$('.mpr-info-wrapper.pinned').removeClass('pinned').find('.material-icons').text('info_outline');
$('.mpr-tooltip-fixed').remove();
var content = $wrapper.attr('data-tooltip');
if (!content) return;
// Pin this one
$wrapper.addClass('pinned');
$wrapper.find('.material-icons').text('close');
// Create pinned tooltip with close button
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed pinned' });
var $closeBtn = $('<button>', { class: 'mpr-tooltip-close', type: 'button' })
.append($('<i>', { class: 'material-icons', text: 'close' }));
$tooltip.append($closeBtn).append(content);
$('body').append($tooltip);
// Close button click
$closeBtn.on('click', function() {
$wrapper.removeClass('pinned');
$wrapper.find('.material-icons').text('info_outline');
$tooltip.remove();
});
// Position
var rect = $wrapper[0].getBoundingClientRect();
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
var top = rect.top - tooltipHeight - 8;
if (left < 10) left = 10;
if (left + tooltipWidth > window.innerWidth - 10) {
left = window.innerWidth - tooltipWidth - 10;
}
if (top < 10) {
top = rect.bottom + 8;
}
$tooltip.css({ top: top, left: left });
});
}
};

View File

@@ -238,7 +238,7 @@
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>';
if (group.count !== undefined) {
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + 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>';
$attrContainer.append(html);
@@ -255,7 +255,7 @@
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>';
if (group.count !== undefined) {
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + 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>';
$featContainer.append(html);
@@ -310,7 +310,7 @@
// Add close button as sibling (outside filter-values-container, inside filter-row-values)
$filterRowValues.find('.btn-close-values').remove();
$filterRowValues.append('<button type="button" class="btn-close-values">' + this.esIcon('close') + '</button>');
$filterRowValues.append('<button type="button" class="btn-close-values"><i class="icon-times"></i></button>');
$filterRowValues.show();
// Scroll into view if needed

View File

@@ -47,13 +47,13 @@
// Group header
html += '<div class="group-header">';
html += '<span class="group-collapse-toggle">' + this.esIcon('expand_less') + '</span>';
html += '<span class="group-collapse-toggle"><i class="icon-chevron-up"></i></span>';
html += '<span class="group-name-wrapper">';
html += '<input type="text" class="group-name-input" value="" placeholder="' + defaultGroupName + '" title="' + (trans.click_to_name || 'Click to name this group') + '">';
html += '<span class="group-count-badge" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '<span class="group-count-badge" style="display:none;"></span>';
html += '</span>';
html += '<button type="button" class="btn-remove-group" title="' + (trans.remove_group || 'Remove group') + '">';
html += this.esIcon('delete');
html += '<i class="icon-trash"></i>';
html += '</button>';
html += '</div>';
@@ -64,17 +64,21 @@
html += '<div class="group-include">';
html += '<div class="section-row">';
html += '<div class="method-selector-wrapper">';
html += '<select class="include-method-select">' + methodOptions + '</select>';
html += '<span class="condition-match-count no-matches">' + this.esIcon('visibility') + ' <span class="preview-count">0</span></span>';
html += '<span class="method-info-placeholder"></span>';
html += '<select class="include-method-select">' + methodOptions + '</select>';
html += '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
html += '</div>';
var noItemsText = trans.no_items_selected || 'No items selected - use search below';
html += '<div class="value-picker include-picker" style="display:none;" data-search-entity="' + blockType + '">';
html += '<div class="entity-chips include-chips" data-placeholder="' + noItemsText + '"></div>';
html += '<div class="chips-wrapper">';
html += '<div class="chips-toolbar" style="display:none;"></div>';
html += '<div class="entity-chips include-chips" data-placeholder="' + noItemsText + '"><span class="chips-empty-state">' + noItemsText + '</span></div>';
html += '<div class="chips-load-more" style="display:none;"></div>';
html += '</div>';
html += '<div class="entity-search-box">';
html += this.esIcon('search', 'entity-search-icon');
html += '<i class="icon-search entity-search-icon"></i>';
html += '<input type="text" class="entity-search-input" placeholder="' + (trans.search_placeholder || 'Search by name, reference, ID...') + '" autocomplete="off">';
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
html += '</div>';
html += '<input type="hidden" class="include-values-data" value="[]">';
html += '</div>';
@@ -84,7 +88,7 @@
// Excludes section (collapsed by default)
html += '<div class="group-excludes">';
html += '<button type="button" class="btn-add-exclude">';
html += this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions');
html += '<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions');
html += '</button>';
html += '</div>';
@@ -106,11 +110,11 @@
html += '<option value="random">' + (trans.sort_random || 'Random') + '</option>';
html += '</select>';
html += '<button type="button" class="btn-sort-dir" data-dir="DESC" title="' + (trans.sort_direction || 'Sort direction') + '">';
html += this.esIcon('sort');
html += '<i class="icon-sort-amount-desc"></i>';
html += '</button>';
html += '</span>';
html += '<span class="group-preview-badge clickable" title="' + (trans.preview_results || 'Preview results') + '">';
html += this.esIcon('visibility') + ' <span class="preview-count"></span>';
html += '<i class="icon-eye"></i> <span class="preview-count"></span>';
html += '</span>';
html += '</div>';
@@ -157,7 +161,7 @@
var self = this;
// Remove all groups from all blocks
this.$wrapper.find('.target-block').each(function() {
this.$wrapper.find('.es-block').each(function() {
var $block = $(this);
var $container = $block.find('.groups-container');
@@ -186,41 +190,105 @@
switchToBlock: function(blockType) {
// Update tabs
this.$wrapper.find('.target-block-tab').removeClass('active');
this.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]').addClass('active');
this.$wrapper.find('.es-block-tab').removeClass('active');
this.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]').addClass('active');
// Update blocks
this.$wrapper.find('.target-block').removeClass('active').hide();
this.$wrapper.find('.target-block[data-block-type="' + blockType + '"]').addClass('active').show();
this.$wrapper.find('.es-block').removeClass('active').hide();
this.$wrapper.find('.es-block[data-block-type="' + blockType + '"]').addClass('active').show();
// Close dropdown if open
this.hideDropdown();
},
/**
* Single-mode badge update: read chip counts from DOM, set directly.
* No AJAX, no spinners, no updateBlockStatus.
*/
_updateSingleModeBadges: function() {
var self = this;
this.$wrapper.find('.es-block-tab').each(function() {
var $tab = $(this);
var blockType = $tab.data('blockType');
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
var $badge = $tab.find('.tab-badge');
// Custom blocks: check input value
if ($block.hasClass('custom-block')) {
var hasVal = false;
$block.find('.custom-block-content').find('input, textarea, select').each(function() {
if ($(this).val() && $(this).val().trim() !== '') { hasVal = true; return false; }
});
if (hasVal) {
if (!$badge.length) { $tab.append('<span class="tab-badge"><i class="icon-check"></i></span>'); }
else { $badge.html('<i class="icon-check"></i>'); }
$tab.addClass('has-data');
} else {
$badge.remove();
$tab.removeClass('has-data');
}
return;
}
// Entity blocks: count chips
var chipCount = $block.find('.entity-chip:not(.entity-chip-loading)').length;
if (chipCount > 0) {
if (!$badge.length) { $tab.append('<span class="tab-badge"><i class="icon-eye"></i> ' + chipCount + '</span>'); }
else { $badge.html('<i class="icon-eye"></i> ' + chipCount); }
$tab.addClass('has-data');
} else {
$badge.remove();
$tab.removeClass('has-data');
}
});
// Also update inline condition count (eye icon next to "Specific products")
this.$wrapper.find('.es-block.active').each(function() {
var $block = $(this);
var chipCount = $block.find('.entity-chip:not(.entity-chip-loading)').length;
var $countEl = $block.find('.condition-match-count').first();
self._setBadgeCount($countEl, chipCount);
var $groupBadge = $block.find('.group-count-badge').first();
if ($groupBadge.length) {
self._setBadgeCount($groupBadge, chipCount);
}
});
},
/**
* Set badge to correct visual state based on count.
* ONE function for ALL badge types — no more class juggling in 20 places.
*/
_setBadgeCount: function($el, count) {
if (!$el || !$el.length) return;
var html = '<i class="icon-eye"></i> ' + count;
if ($el.html() !== html) $el.html(html);
$el.removeClass('loading loading-count no-matches clickable');
$el.addClass(count > 0 ? 'clickable' : 'no-matches');
$el.show();
},
updateTabBadges: function() {
var self = this;
// Collect all block types with data and set loading state
var blockTypesWithData = [];
this.$wrapper.find('.target-block-tab').each(function() {
this.$wrapper.find('.es-block-tab').each(function() {
var $tab = $(this);
var blockType = $tab.data('blockType');
var $block = self.$wrapper.find('.target-block[data-block-type="' + blockType + '"]');
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
var groupCount = $block.find('.selection-group').length;
// Update or add badge
var $badge = $tab.find('.tab-badge');
if (groupCount > 0) {
// Show loading state first
if ($badge.length) {
$badge.addClass('loading').html(self.esIcon('progress_activity', 'es-spin'));
} else {
$tab.append('<span class="tab-badge loading">' + self.esIcon('progress_activity', 'es-spin') + '</span>');
// Has data — show spinner ONLY if no badge exists yet.
// If badge already has a count, keep it visible while AJAX refreshes.
if (!$badge.length) {
$tab.append('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
}
$tab.addClass('has-data');
blockTypesWithData.push(blockType);
} else if ($block.hasClass('custom-block')) {
// Custom blocks: check if any input/textarea/select has a non-empty value
var hasCustomValue = false;
$block.find('.custom-block-content').find('input, textarea, select').each(function() {
if ($(this).val() && $(this).val().trim() !== '') {
@@ -229,11 +297,8 @@
}
});
if (hasCustomValue) {
if ($badge.length) {
$badge.removeClass('loading').html(self.esIcon('check'));
} else {
$tab.append('<span class="tab-badge">' + self.esIcon('check') + '</span>');
}
if (!$badge.length) { $tab.append('<span class="tab-badge"><i class="icon-check"></i></span>'); }
else { $badge.removeClass('loading').html('<i class="icon-check"></i>'); }
$tab.addClass('has-data');
} else {
$badge.remove();
@@ -245,10 +310,8 @@
}
});
// Update target switch state based on whether any data exists
this.updateTargetSwitchState();
// Fetch all counts in a single bulk request
if (blockTypesWithData.length > 0) {
this.fetchAllCounts(blockTypesWithData);
}
@@ -262,7 +325,7 @@
// Check if any block has data
var hasData = false;
this.$wrapper.find('.target-block').each(function() {
this.$wrapper.find('.es-block').each(function() {
if ($(this).find('.selection-group').length > 0) {
hasData = true;
return false; // break
@@ -284,7 +347,6 @@
fetchAllCounts: function(blockTypes) {
var self = this;
// Read saved data from hidden input
var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
var savedData = {};
try {
@@ -302,17 +364,25 @@
}
});
// If no valid conditions, remove loading spinners
// Blocks with groups but no matchable conditions: show 0
blockTypes.forEach(function(blockType) {
if (!(blockType in conditions)) {
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
var $badge = $tab.find('.tab-badge');
if (!$badge.length) { $badge = $('<span class="tab-badge"></span>'); $tab.append($badge); }
self._setBadgeCount($badge, 0);
$tab.addClass('has-data');
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
self._setBadgeCount($block.find('.condition-match-count').first(), 0);
self._setBadgeCount($block.find('.group-count-badge').first(), 0);
}
});
if (Object.keys(conditions).length === 0) {
blockTypes.forEach(function(blockType) {
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
$tab.find('.tab-badge').remove();
$tab.removeClass('has-data');
});
return;
}
// Single bulk AJAX request for all counts
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
@@ -325,23 +395,26 @@
},
success: function(response) {
if (response.success && response.counts) {
// Update each tab with its count
Object.keys(response.counts).forEach(function(blockType) {
var count = response.counts[blockType];
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
var $badge = $tab.find('.tab-badge');
if ($badge.length) {
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + count);
// Store preview data for later popover use
$tab.data('previewData', { count: count, success: true });
}
self._setBadgeCount($badge, count);
$tab.data('previewData', { count: count, success: true });
});
// Handle any block types not in response (set count to 0 or remove badge)
// Also update inline + group badges
Object.keys(response.counts).forEach(function(blockType) {
var count = response.counts[blockType];
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
self._setBadgeCount($block.find('.condition-match-count').first(), count);
self._setBadgeCount($block.find('.group-count-badge').first(), count);
});
blockTypes.forEach(function(blockType) {
if (!(blockType in response.counts)) {
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
$tab.find('.tab-badge').remove();
$tab.removeClass('has-data');
}
@@ -352,7 +425,7 @@
console.error('[EntitySelector] Bulk preview failed:', response.error || 'Unknown error');
// Remove loading spinners on error
blockTypes.forEach(function(blockType) {
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
$tab.find('.tab-badge').remove();
});
}
@@ -361,7 +434,7 @@
console.error('[EntitySelector] Bulk AJAX error:', status, error);
// Remove loading spinners on error
blockTypes.forEach(function(blockType) {
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
$tab.find('.tab-badge').remove();
});
}
@@ -397,10 +470,10 @@
// Show loading state
var $badge = $tab.find('.tab-badge');
if (!$badge.length) {
$badge = $('<span class="tab-badge loading">' + this.esIcon('progress_activity', 'es-spin') + '</span>');
$badge = $('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
$tab.append($badge);
} else {
$badge.addClass('loading').html(this.esIcon('progress_activity', 'es-spin'));
$badge.addClass('loading').html('<i class="icon-spinner icon-spin"></i>');
}
$tab.addClass('has-data');
@@ -421,7 +494,7 @@
success: function(response) {
if (response.success) {
var $badge = $tab.find('.tab-badge');
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + response.count);
$badge.removeClass('loading').html('<i class="icon-eye"></i> ' + response.count);
// Store preview data for popover
$tab.data('previewData', response);
@@ -446,7 +519,7 @@
var total = 0;
// Sum up all tab badge counts
this.$wrapper.find('.target-block-tab .tab-badge').each(function() {
this.$wrapper.find('.es-block-tab .tab-badge').each(function() {
var $badge = $(this);
if (!$badge.hasClass('loading')) {
var count = parseInt($badge.text(), 10);
@@ -473,7 +546,7 @@
if (!$toggle.length) return;
var $checkbox = $toggle.find('.show-all-checkbox');
var hasData = this.$wrapper.find('.target-block-tab.has-data').length > 0;
var hasData = this.$wrapper.find('.es-block-tab.has-data').length > 0;
// If there's data, uncheck (not showing to all), otherwise check
$checkbox.prop('checked', !hasData);
@@ -568,16 +641,11 @@
var self = this;
var data = {};
console.log('[EntitySelector] serializeAllBlocks called');
this.$wrapper.find('.target-block').each(function() {
this.$wrapper.find('.es-block').each(function() {
var $block = $(this);
var blockType = $block.data('blockType');
var groups = self.getBlockGroups($block);
console.log('[EntitySelector] Block:', blockType, 'Groups:', groups.length);
// Groups now contain their own modifiers, no block-level modifiers
if (groups.length > 0) {
data[blockType] = { groups: groups };
}
@@ -585,37 +653,35 @@
self.updateBlockStatus($block);
});
// Update hidden input first
var $input = this.$wrapper.find('input[name="' + this.config.name + '"]');
var jsonData = JSON.stringify(data);
console.log('[EntitySelector] Hidden input name:', this.config.name);
console.log('[EntitySelector] Hidden input found:', $input.length);
console.log('[EntitySelector] Serialized data:', jsonData.substring(0, 500));
// Skip if data hasn't changed since last serialization
if (this._lastSerializedData === jsonData) return;
this._lastSerializedData = jsonData;
$input.val(jsonData);
// Then update tab badges (reads from hidden input)
this.updateTabBadges();
// Debounced update of condition count - only for changed row if specified
if (this.countUpdateTimeout) {
clearTimeout(this.countUpdateTimeout);
// Single mode: update badges from chip count directly, no AJAX
if (this.config.mode === 'single') {
this._updateSingleModeBadges();
return;
}
this.countUpdateTimeout = setTimeout(function() {
// Multi mode: debounce ALL visual updates so rapid serialize calls collapse
if (this._visualUpdateTimer) clearTimeout(this._visualUpdateTimer);
this._visualUpdateTimer = setTimeout(function() {
self.updateTabBadges();
if ($changedRow && $changedRow.length) {
// Update the specific row that changed
self.updateConditionCount($changedRow);
// Also update the group total count (include - excludes)
var $group = $changedRow.closest('.selection-group');
if ($group.length) {
self.updateGroupTotalCount($group);
}
} else {
// Fallback: update all counts (initial load, structure changes)
self.updateAllConditionCounts();
}
}, 500);
}, 300);
},
getBlockGroups: function($block) {
@@ -852,9 +918,9 @@
var conditionIndex = 0;
// Collect all conditions from all active groups
this.$wrapper.find('.target-block.active .selection-group').each(function() {
this.$wrapper.find('.es-block.active .selection-group').each(function() {
var $group = $(this);
var $block = $group.closest('.target-block');
var $block = $group.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
// Process include row
@@ -905,10 +971,9 @@
$countEl.removeClass('no-matches clickable');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
self._setBadgeCount($countEl, 0);
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
self._setBadgeCount($countEl, count);
}
}
});
@@ -920,7 +985,7 @@
Object.keys(conditionElements).forEach(function(id) {
var $countEl = conditionElements[id];
if ($countEl && $countEl.length) {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
}
@@ -931,9 +996,7 @@
* Extract condition data from a row for bulk counting
*/
getConditionData: function($row, blockType) {
console.log('[getConditionData] Called with blockType:', blockType);
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
console.log('[getConditionData] $countEl found:', $countEl.length);
if (!$countEl.length) return null;
var isExclude = $row.hasClass('exclude-row');
@@ -942,23 +1005,18 @@
: $row.find('.include-method-select');
var method = $methodSelect.val();
console.log('[getConditionData] method:', method);
if (!method) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return null;
}
var $picker = isExclude
? $row.find('.exclude-picker')
: $row.find('.include-picker');
console.log('[getConditionData] $picker found:', $picker.length, 'data-value-type attr:', $picker.attr('data-value-type'));
var valueType = $picker.data('valueType') || $picker.attr('data-value-type') || 'none';
console.log('[getConditionData] valueType:', valueType);
// Special case: "All countries" method - needs separate handling for holidays
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
console.log('[getConditionData] All countries detected - triggering updateConditionCount');
// Trigger separate update for this special case (uses nested AJAX)
var self = this;
setTimeout(function() {
@@ -970,7 +1028,6 @@
// Special case: Specific countries with entity_search - needs holiday counting, not entity counting
var searchEntity = $picker.attr('data-search-entity') || '';
if (blockType === 'countries' && valueType === 'entity_search' && searchEntity === 'countries') {
console.log('[getConditionData] Specific countries detected - triggering updateConditionCount for holiday counting');
var self = this;
setTimeout(function() {
self.updateConditionCount($row, blockType);
@@ -978,9 +1035,14 @@
return null; // Skip bulk processing, handled separately
}
// Hide badge for other "all" type methods (valueType === 'none') since they don't filter
// "All" type methods (valueType === 'none') — store conditionData for preview, count comes from fetchAllCounts
if (valueType === 'none') {
$countEl.hide();
$countEl.data('conditionData', {
method: method,
values: [],
blockType: blockType,
isExclude: isExclude
});
return null;
}
@@ -994,13 +1056,12 @@
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
));
if (valueType !== 'boolean' && hasNoValues) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return null;
}
// Show loading spinner
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Show loading state — keep existing count visible, just dim it
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
// Store condition data on badge for popover
$countEl.data('conditionData', {
@@ -1022,7 +1083,7 @@
updateGroupCounts: function($group) {
var self = this;
var $block = $group.closest('.target-block');
var $block = $group.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
// Update include count
@@ -1048,7 +1109,6 @@
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
if (!$countEl.length) {
console.log('[updateConditionCount] No $countEl found');
return;
}
@@ -1058,10 +1118,8 @@
: $row.find('.include-method-select');
var method = $methodSelect.val();
console.log('[updateConditionCount] method:', method, 'isExclude:', isExclude);
if (!method) {
console.log('[updateConditionCount] No method, hiding badge');
$countEl.hide();
self._setBadgeCount($countEl, 0);
return;
}
@@ -1074,17 +1132,13 @@
// Get the block type to check if this is a countries block
if (!blockType) {
var $block = $row.closest('.target-block');
var $block = $row.closest('.es-block');
blockType = $block.data('blockType') || 'products';
}
console.log('[updateConditionCount] valueType:', valueType, 'searchEntity:', searchEntity, 'blockType:', blockType, 'method:', method);
// Special case: "All countries" method - fetch holidays for all countries
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
console.log('[updateConditionCount] All countries method - fetching all country holidays');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches country-holidays').show();
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
// First fetch all active country IDs, then get holidays
$.ajax({
@@ -1103,7 +1157,6 @@
var items = response.results || response.items || [];
if (response && response.success && items.length > 0) {
var allCountryIds = items.map(function(item) { return item.id; });
console.log('[updateConditionCount] Found', allCountryIds.length, 'countries, fetching holidays');
// Store condition data for click handler
$countEl.data('conditionData', {
@@ -1129,42 +1182,44 @@
count_only: 1
},
success: function(holidayResponse) {
console.log('[updateConditionCount] All countries holiday response:', holidayResponse);
if (holidayResponse && holidayResponse.success) {
var count = holidayResponse.total_count || 0;
$countEl.removeClass('no-matches clickable');
$countEl.addClass('country-holidays');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
self._setBadgeCount($countEl, 0);
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
self._setBadgeCount($countEl, count);
}
$countEl.data('countriesInfo', holidayResponse.countries || []);
} else {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
} else {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
return;
}
// Hide badge for other "all" type methods (valueType === 'none') since they don't filter
// "All" type methods (valueType === 'none') — store conditionData for preview
if (valueType === 'none') {
console.log('[updateConditionCount] valueType is none, hiding badge');
$countEl.hide();
$countEl.data('conditionData', {
method: method,
values: [],
blockType: blockType,
isExclude: isExclude
});
return;
}
@@ -1177,25 +1232,22 @@
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
));
if (valueType !== 'boolean' && hasNoValues) {
$countEl.hide();
self._setBadgeCount($countEl, 0);
return;
}
if (!blockType) {
var $block = $row.closest('.target-block');
var $block = $row.closest('.es-block');
blockType = $block.data('blockType') || 'products';
}
// Check if this is a country selection - show holiday count instead
var isCountrySelection = (searchEntity === 'countries' && valueType === 'entity_search');
console.log('[updateConditionCount] isCountrySelection:', isCountrySelection, 'values:', values);
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches country-holidays').show();
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
// For countries, fetch holiday count
if (isCountrySelection && Array.isArray(values) && values.length > 0) {
console.log('[updateConditionCount] Fetching holiday count for countries:', values);
$countEl.data('conditionData', {
method: method,
values: values,
@@ -1217,28 +1269,24 @@
count_only: 1
},
success: function(response) {
console.log('[updateConditionCount] Holiday response:', response);
if (response && response.success) {
var count = response.total_count || 0;
console.log('[updateConditionCount] Holiday count:', count);
$countEl.removeClass('no-matches clickable');
$countEl.addClass('country-holidays');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
self._setBadgeCount($countEl, 0);
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
self._setBadgeCount($countEl, count);
}
// Store countries info for popover
$countEl.data('countriesInfo', response.countries || []);
} else {
console.log('[updateConditionCount] Holiday response failed:', response);
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
return;
@@ -1270,24 +1318,23 @@
$countEl.removeClass('no-matches clickable');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
self._setBadgeCount($countEl, 0);
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
self._setBadgeCount($countEl, count);
}
} else {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
},
error: function() {
$countEl.hide().removeClass('clickable');
self._setBadgeCount($countEl, 0);
}
});
},
updateGroupTotalCount: function($group) {
var self = this;
var $block = $group.closest('.target-block');
var $block = $group.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
var $badge = $group.find('.group-header .group-count-badge');
var $limitInput = $group.find('.group-modifier-limit');
@@ -1303,7 +1350,7 @@
}
// Show loading
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
if (!$badge.hasClass('clickable')) { $badge.addClass('loading-count'); } $badge.show();
$.ajax({
url: this.config.ajaxUrl,
@@ -1321,13 +1368,14 @@
var finalCount = response.final_count || 0;
var excludeCount = response.exclude_count || 0;
// Update badge with eye icon and count
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
if (excludeCount > 0) {
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
}
$badge.html(badgeHtml);
$badge.addClass('clickable').show();
if ($badge.html() !== badgeHtml) {
$badge.html(badgeHtml);
}
self._setBadgeCount($badge, finalCount);
// Store group data on badge for preview popover
$badge.data('groupData', groupData);
@@ -1345,7 +1393,7 @@
$previewBadge.text(displayCount);
}
} else {
$badge.hide().removeClass('clickable');
self._setBadgeCount($badge, 0);
$limitInput.attr('placeholder', '');
}
},
@@ -1363,7 +1411,7 @@
// Build the full excludes structure with first row
var html = '<div class="except-separator">';
html += '<span class="except-label">' + this.esIcon('block') + ' ' + (trans.except || 'EXCEPT') + '</span>';
html += '<span class="except-label"><i class="icon-ban"></i> ' + (trans.except || 'EXCEPT') + '</span>';
html += '</div>';
html += '<div class="exclude-rows-container">';
@@ -1371,7 +1419,7 @@
html += '</div>';
html += '<button type="button" class="btn-add-another-exclude">';
html += this.esIcon('add') + ' ' + (trans.add_another_exception || 'Add another exception');
html += '<i class="icon-plus"></i> ' + (trans.add_another_exception || 'Add another exception');
html += '</button>';
$excludesDiv.addClass('has-excludes').html(html);
@@ -1442,11 +1490,11 @@
html += '<div class="exclude-header-row">';
html += '<div class="method-selector-wrapper">';
html += '<select class="exclude-method-select">' + excludeMethodOptions + '</select>';
html += '<span class="condition-match-count no-matches">' + this.esIcon('visibility') + ' <span class="preview-count">0</span></span>';
html += '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
html += '<span class="method-info-placeholder"></span>';
html += '</div>';
html += '<button type="button" class="btn-remove-exclude-row" title="' + (trans.remove_this_exception || 'Remove this exception') + '">';
html += this.esIcon('delete');
html += '<i class="icon-trash"></i>';
html += '</button>';
html += '</div>';
@@ -1472,7 +1520,7 @@
var $excludesDiv = $group.find('.group-excludes');
$excludesDiv.removeClass('has-excludes').html(
'<button type="button" class="btn-add-exclude">' +
this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions') +
'<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions') +
'</button>'
);
// Unlock the method selector since no excludes exist
@@ -1576,11 +1624,15 @@
switch (valueType) {
case 'entity_search':
var noItemsText = trans.no_items_selected || 'No items selected - use search below';
html += '<div class="entity-chips ' + chipsClass + '" data-placeholder="' + this.escapeAttr(noItemsText) + '"></div>';
html += '<div class="chips-wrapper">';
html += '<div class="chips-toolbar" style="display:none;"></div>';
html += '<div class="entity-chips ' + chipsClass + '" data-placeholder="' + this.escapeAttr(noItemsText) + '"><span class="chips-empty-state">' + this.escapeHtml(noItemsText) + '</span></div>';
html += '<div class="chips-load-more" style="display:none;"></div>';
html += '</div>';
html += '<div class="entity-search-box">';
html += this.esIcon('search', 'entity-search-icon');
html += '<i class="icon-search entity-search-icon"></i>';
html += '<input type="text" class="entity-search-input" placeholder="' + this.escapeAttr(trans.search_placeholder || 'Search by name, reference, ID...') + '" autocomplete="off">';
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
break;
@@ -1607,12 +1659,10 @@
html += '<div class="pattern-tag draft-tag" data-case-sensitive="0">';
html += '<button type="button" class="btn-toggle-case" title="' + this.escapeAttr(trans.case_insensitive || 'Case insensitive - click to toggle') + '"><span class="case-icon">aa</span></button>';
html += '<input type="text" class="pattern-input" value="" placeholder="' + this.escapeAttr(trans.enter_pattern || 'e.g. *cotton*') + '">';
html += '<span class="pattern-match-count" title="' + this.escapeAttr(trans.click_to_preview || 'Click to preview matches') + '">' + this.esIcon('visibility') + ' <span class="count-value"></span></span>';
html += '<button type="button" class="btn-add-pattern" title="' + this.escapeAttr(trans.add_pattern || 'Add pattern (Enter)') + '">' + this.esIcon('add') + '</button>';
html += '<span class="pattern-match-count" title="' + this.escapeAttr(trans.click_to_preview || 'Click to preview matches') + '"><i class="icon-eye"></i> <span class="count-value"></span></span>';
html += '<button type="button" class="btn-add-pattern" title="' + this.escapeAttr(trans.add_pattern || 'Add pattern (Enter)') + '"><i class="icon-plus"></i></button>';
html += '</div>';
html += '<span class="mpr-info-wrapper" data-details="' + this.escapeAttr(tooltipContent) + '">';
html += this.esIcon('info');
html += '</span>';
html += this._buildInfoTooltip(tooltipContent, 'details');
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
break;
@@ -1633,7 +1683,7 @@
html += '<input type="number" class="range-min-input" value="" placeholder="' + this.escapeAttr(trans.min || 'Min') + '" step="0.01">';
html += '<span class="range-separator">-</span>';
html += '<input type="number" class="range-max-input" value="" placeholder="' + this.escapeAttr(trans.max || 'Max') + '" step="0.01">';
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '">' + this.esIcon('add') + '</button>';
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '"><i class="icon-plus"></i></button>';
html += '</div>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
@@ -1701,7 +1751,7 @@
html += '</div>';
}
html += '<div class="combination-groups-container">';
html += '<span class="combination-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
html += '<span class="combination-loading"><i class="icon-spinner icon-spin"></i> ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
html += '</div>';
html += '</div>';
// Store mode along with attributes: { mode: 'products'|'combinations', attributes: { groupId: [valueIds] } }
@@ -1747,14 +1797,28 @@
}
},
getSortIconName: function(sortBy, sortDir) {
getSortIconClass: function(sortBy, sortDir) {
var isAsc = (sortDir === 'ASC');
switch (sortBy) {
case 'name':
return 'sort_by_alpha';
return isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
case 'price':
case 'quantity':
case 'product_count':
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'date_add':
case 'newest_products':
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'sales':
case 'total_sales':
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
case 'position':
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'random':
return 'shuffle';
return 'icon-random';
default:
return 'sort';
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
}
},
@@ -1799,7 +1863,7 @@
$btn.attr('data-sort', newSort);
$btn.attr('data-dir', newDir);
$btn.attr('title', newLabel + ' ' + (newDir === 'DESC' ? '↓' : '↑'));
$btn.find('i').replaceWith(this.esIcon(this.getSortIconName(newSort, newDir)));
$btn.find('i').attr('class', this.getSortIconClass(newSort, newDir));
},
// Validation
@@ -1811,7 +1875,7 @@
// Check if any block has data (groups with selections)
var hasData = false;
this.$wrapper.find('.target-block').each(function() {
this.$wrapper.find('.es-block').each(function() {
if ($(this).find('.selection-group').length > 0) {
hasData = true;
return false; // break
@@ -1839,7 +1903,7 @@
// Add error message after header
var $error = $('<div>', {
class: 'trait-validation-error',
html: this.esIcon('warning') + ' ' + message
html: '<i class="icon-warning"></i> ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
@@ -1849,8 +1913,8 @@
}, 300);
// Expand the trait if collapsed
if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
this.$wrapper.find('.condition-trait-body').slideDown(200);
if (!this.$wrapper.find('.condition-trait-body').hasClass('es-expanded')) {
this.$wrapper.find('.condition-trait-body').addClass('es-expanded');
this.$wrapper.removeClass('collapsed');
}
},

View File

@@ -61,7 +61,7 @@
var self = this;
this.$wrapper.find('.selection-group').each(function() {
var $group = $(this);
var $block = $group.closest('.target-block');
var $block = $group.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
// Include method info
@@ -88,18 +88,24 @@
if (!$select.length || $select.data('methodDropdownInit')) {
return;
}
// Skip if only 1 option (nothing to switch between)
if ($select.find('option').length <= 1) {
$select.data('methodDropdownInit', true);
return;
}
$select.data('methodDropdownInit', true);
$select.addClass('method-select-hidden');
var $selectedOption = $select.find('option:selected');
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
var selectedIcon = $selectedOption.data('icon') || 'icon-caret-down';
var selectedLabel = $selectedOption.text();
var triggerHtml = '<div class="method-dropdown-trigger">';
triggerHtml += this.esIcon(selectedIcon, 'method-trigger-icon');
triggerHtml += '<i class="' + this.escapeAttr(selectedIcon) + ' method-trigger-icon"></i>';
triggerHtml += '<span class="method-trigger-label">' + this.escapeHtml(selectedLabel) + '</span>';
triggerHtml += this.esIcon('arrow_drop_down', 'method-trigger-caret');
triggerHtml += '<i class="icon-caret-down method-trigger-caret"></i>';
triggerHtml += '</div>';
var $trigger = $(triggerHtml);
@@ -127,10 +133,10 @@
*/
updateMethodTrigger: function($select, $trigger) {
var $selectedOption = $select.find('option:selected');
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
var selectedIcon = $selectedOption.data('icon') || 'icon-caret-down';
var selectedLabel = $selectedOption.text();
$trigger.find('.method-trigger-icon').replaceWith(this.esIcon(selectedIcon, 'method-trigger-icon'));
$trigger.find('.method-trigger-icon').attr('class', selectedIcon + ' method-trigger-icon');
$trigger.find('.method-trigger-label').text(selectedLabel);
},
@@ -194,16 +200,16 @@
// Render ungrouped options first
$select.children('option').each(function() {
var $el = $(this);
var icon = $el.data('icon') || 'star';
var icon = $el.data('icon') || 'icon-asterisk';
var label = $el.text();
var value = $el.val();
var isSelected = $el.is(':selected');
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
html += self.esIcon(icon, 'method-item-icon');
html += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += self.esIcon('check', 'method-item-check');
html += '<i class="icon-check method-item-check"></i>';
}
html += '</div>';
});
@@ -219,16 +225,16 @@
$optgroup.children('option').each(function() {
var $el = $(this);
var icon = $el.data('icon') || 'settings';
var icon = $el.data('icon') || 'icon-cog';
var label = $el.text();
var value = $el.val();
var isSelected = $el.is(':selected');
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
html += self.esIcon(icon, 'method-item-icon');
html += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += self.esIcon('check', 'method-item-check');
html += '<i class="icon-check method-item-check"></i>';
}
html += '</div>';
});
@@ -384,13 +390,13 @@
type: 'button',
class: 'comb-toolbar-btn comb-select-all',
title: trans.select_all || 'Select all',
html: self.esIcon('check_box')
html: '<i class="icon-check-square-o"></i>'
}));
$toolbar.append($('<button>', {
type: 'button',
class: 'comb-toolbar-btn comb-select-none',
title: trans.clear || 'Clear',
html: self.esIcon('check_box_outline_blank')
html: '<i class="icon-square-o"></i>'
}));
$toolbar.append($('<input>', {
type: 'text',
@@ -404,7 +410,7 @@
});
$valuesContainer.append($('<span>', {
class: 'comb-attr-loading',
html: self.esIcon('progress_activity', 'es-spin')
html: '<i class="icon-spinner icon-spin"></i>'
}));
$groupDiv.append($groupHeader);
@@ -582,17 +588,7 @@
var helpContent = blockHelp[method] || this.getBuiltInMethodHelp(method);
if (helpContent) {
var $infoWrapper = $('<span>', {
class: 'mpr-info-wrapper',
'data-details': helpContent
});
$infoWrapper.append($(this.esIcon('info')));
$placeholder.append($infoWrapper);
// Let prestashop-admin info-tooltip.js handle this element
if (window.MPRInfoTooltip) {
window.MPRInfoTooltip.init();
}
$placeholder.append(this._buildInfoTooltip(helpContent, 'tooltip'));
}
},
@@ -851,7 +847,7 @@
$wrapper.addClass('selector-locked');
if (!$wrapper.find('.lock-indicator').length) {
var lockHtml = '<span class="mpr-info-wrapper lock-indicator">' +
this.esIcon('lock') +
'<i class="icon-lock"></i>' +
'<span class="mpr-tooltip">' +
(trans.remove_excludes_first || 'Remove all exceptions to change selection type') +
'</span>' +

View File

@@ -19,7 +19,7 @@
var self = this;
var total = 0;
this.$wrapper.find('.target-block-tab .tab-badge').each(function() {
this.$wrapper.find('.es-block-tab .tab-badge').each(function() {
var $badge = $(this);
if (!$badge.hasClass('loading')) {
var count = parseInt($badge.text(), 10);
@@ -37,7 +37,7 @@
$countValue.text(total);
} else {
// Fallback: set HTML with icon
$totalBadge.html(self.esIcon('visibility') + ' <span class="count-value">' + total + '</span>');
$totalBadge.html('<i class="icon-eye"></i> <span class="count-value">' + total + '</span>');
}
$totalBadge.show();
} else {
@@ -52,7 +52,7 @@
if (!$toggle.length) return;
var $checkbox = $toggle.find('.show-all-checkbox');
var hasData = this.$wrapper.find('.target-block-tab.has-data').length > 0;
var hasData = this.$wrapper.find('.es-block-tab.has-data').length > 0;
$checkbox.prop('checked', !hasData);
},
@@ -86,12 +86,12 @@
var previewType = options.previewType || 'default';
// Build popover HTML
var html = '<div class="target-preview-popover preview-type-' + previewType + '">';
var html = '<div class="es-preview-popover preview-type-' + previewType + '">';
// Header with count and close button
html += '<div class="preview-header">';
html += '<span class="preview-count">' + totalCount + ' ' + entityLabel + '</span>';
html += '<button type="button" class="preview-close">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="preview-close"><i class="icon-times"></i></button>';
html += '</div>';
// Filter input
@@ -119,7 +119,7 @@
html += '<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>';
html += '</select>';
html += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">' + remaining + '</span> ' + (trans.remaining || 'remaining') + '</span>';
html += '<button type="button" class="btn-load-more">' + self.esIcon('add') + '</button>';
html += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
html += '</div>';
html += '</div>';
}
@@ -181,7 +181,7 @@
if ($btn.hasClass('loading')) return;
$btn.addClass('loading');
$btn.find('i').replaceWith(self.esIcon('progress_activity', 'es-spin'));
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
$select.prop('disabled', true);
// Get selected load count
@@ -236,7 +236,7 @@
// Reset button state
$btn.removeClass('loading');
$btn.find('i').replaceWith(this.esIcon('add'));
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$select.prop('disabled', false);
// Update remaining count
@@ -286,7 +286,7 @@
if (item.image) {
html += '<img src="' + this.escapeAttr(item.image) + '" class="preview-item-image" alt="">';
} else {
html += '<div class="preview-item-icon">' + self.esIcon('inventory_2') + '</div>';
html += '<div class="preview-item-icon"><i class="material-icons">inventory_2</i></div>';
}
// Info section
@@ -378,7 +378,7 @@
$list.addClass('filtering');
// Add overlay if not exists
if (!$list.find('.filter-loading-overlay').length) {
$list.append('<div class="filter-loading-overlay">' + this.esIcon('progress_activity', 'es-spin') + '</div>');
$list.append('<div class="filter-loading-overlay"><i class="icon-spinner icon-spin"></i></div>');
}
} else {
$list.removeClass('filtering');
@@ -430,7 +430,7 @@
var $select = $controls.find('.load-more-select');
$btn.removeClass('loading');
$btn.find('i').replaceWith(self.esIcon('add'));
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$select.prop('disabled', false);
$controls.find('.remaining-count').text(remaining);
@@ -453,7 +453,7 @@
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">' + self.esIcon('add') + '</button>';
footerHtml += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
footerHtml += '</div>';
footerHtml += '</div>';
@@ -471,7 +471,7 @@
if ($btn.hasClass('loading')) return;
$btn.addClass('loading');
$btn.find('i').replaceWith(self.esIcon('progress_activity', 'es-spin'));
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
$select.prop('disabled', true);
var loadCount = parseInt($select.val(), 10) || 20;
@@ -743,7 +743,7 @@
var $controls = $btn.closest('.load-more-controls');
var $select = $controls.find('.load-more-select');
$btn.removeClass('loading');
$btn.find('i').replaceWith(self.esIcon('add'));
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$select.prop('disabled', false);
}
});
@@ -892,7 +892,7 @@
var $controls = $btn.closest('.load-more-controls');
var $select = $controls.find('.load-more-select');
$btn.removeClass('loading');
$btn.find('i').replaceWith(self.esIcon('add'));
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$select.prop('disabled', false);
}
});
@@ -910,7 +910,7 @@
}
if (!blockType) {
var $block = $badge.closest('.target-block');
var $block = $badge.closest('.es-block');
blockType = $block.data('blockType') || 'products';
}
@@ -1247,7 +1247,7 @@
var isProducts = (ctx.entityType === 'categories');
var action = isProducts ? 'previewCategoryProducts' : 'previewCategoryPages';
$btn.prop('disabled', true).find('i').addClass('es-spin');
$btn.prop('disabled', true).find('i').addClass('icon-spin');
$.ajax({
url: this.config.ajaxUrl,
@@ -1263,7 +1263,7 @@
query: this.previewFilterQuery || ''
},
success: function(response) {
$btn.prop('disabled', false).find('i').removeClass('es-spin');
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
if (response.success && response.items) {
self.appendPreviewItems(response.items);
@@ -1275,7 +1275,7 @@
}
},
error: function() {
$btn.prop('disabled', false).find('i').removeClass('es-spin');
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
}
});
},
@@ -1335,13 +1335,13 @@
html += '<div class="pattern-preview-modal">';
html += '<div class="pattern-preview-header">';
html += '<span class="pattern-preview-title">';
html += this.esIcon('visibility') + ' ' + (trans.preview || 'Preview') + ': <code>' + this.escapeHtml(pattern) + '</code>';
html += '<i class="icon-eye"></i> ' + (trans.preview || 'Preview') + ': <code>' + this.escapeHtml(pattern) + '</code>';
html += '</span>';
html += '<span class="pattern-preview-count">' + count + ' ' + (count === 1 ? entityLabelSingular : entityLabelPlural) + '</span>';
html += '<button type="button" class="pattern-preview-close">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="pattern-preview-close"><i class="icon-times"></i></button>';
html += '</div>';
html += '<div class="pattern-preview-content">';
html += '<div class="pattern-preview-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
html += '<div class="pattern-preview-loading"><i class="icon-spinner icon-spin"></i> ' + (trans.loading || 'Loading...') + '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
@@ -1414,10 +1414,71 @@
// =========================================================================
refreshGroupPreviewIfOpen: function($group) {
// Check if preview is for this group and refresh if needed
if (!this.$activeBadge || !this.$previewPopover) {
return;
}
var $badge = $group.find('.group-count-badge.popover-open, .group-preview-badge.popover-open');
if (!$badge.length) {
return;
}
var self = this;
var $block = $badge.closest('.es-block');
var blockType = $block.data('blockType') || 'products';
var groupData = this.serializeGroup($group, blockType);
if (!groupData || !groupData.include) {
return;
}
// Show loading state in list
var $list = this.$previewPopover.find('.preview-list');
$list.css('opacity', '0.5');
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewGroupItems',
trait: 'EntitySelector',
group_data: JSON.stringify(groupData),
block_type: blockType,
limit: 10
},
success: function(response) {
if (response.success && self.$previewPopover) {
// Update count in header
var blockConfig = self.config.blocks && self.config.blocks[blockType] ? self.config.blocks[blockType] : {};
var entityLabel = blockConfig.entity_label_plural || 'products';
self.$previewPopover.find('.preview-header .preview-count').text(response.count + ' ' + entityLabel);
// Rebuild list items using existing renderer
$list.html(self.renderPreviewItems(response.items || [])).css('opacity', '1');
// Update context
if (self.previewContext) {
self.previewContext.groupData = groupData;
}
// Update load more
var $footer = self.$previewPopover.find('.preview-footer');
if (response.hasMore) {
var remaining = response.count - (response.items || []).length;
$footer.find('.remaining-count').text(remaining);
$footer.show();
} else {
$footer.hide();
}
} else {
$list.css('opacity', '1');
}
},
error: function() {
$list.css('opacity', '1');
}
});
},
/**
@@ -1463,7 +1524,7 @@
// Collect all entity types with data
var summaryItems = [];
this.$wrapper.find('.target-block-tab.has-data').each(function() {
this.$wrapper.find('.es-block-tab.has-data').each(function() {
var $tab = $(this);
var blockType = $tab.data('blockType');
var $tabBadge = $tab.find('.tab-badge');
@@ -1472,7 +1533,7 @@
if (count > 0) {
var blockConfig = self.config.blocks && self.config.blocks[blockType] ? self.config.blocks[blockType] : {};
var icon = $tab.find('.tab-label').prev('i').text() || 'widgets';
var icon = $tab.find('.tab-label').prev('i').attr('class') || 'icon-cube';
var label = $tab.find('.tab-label').text() || blockType;
summaryItems.push({
@@ -1486,7 +1547,7 @@
// Build popover HTML
var totalCount = parseInt($badge.find('.count-value').text(), 10) || 0;
var popoverHtml = '<div class="target-preview-popover total-preview-popover">';
var popoverHtml = '<div class="es-preview-popover total-preview-popover">';
popoverHtml += '<div class="preview-popover-header">';
popoverHtml += '<span class="preview-popover-title">' + (trans.total_summary || 'Selection Summary') + '</span>';
popoverHtml += '<span class="preview-popover-count">' + totalCount + ' ' + (trans.total_items || 'total items') + '</span>';
@@ -1497,7 +1558,7 @@
for (var i = 0; i < summaryItems.length; i++) {
var item = summaryItems[i];
popoverHtml += '<li class="total-summary-item" data-block-type="' + item.blockType + '">';
popoverHtml += this.esIcon(item.icon);
popoverHtml += '<i class="' + self.escapeAttr(item.icon) + '"></i>';
popoverHtml += '<span class="summary-item-label">' + self.escapeHtml(item.label) + '</span>';
popoverHtml += '<span class="summary-item-count">' + item.count + '</span>';
popoverHtml += '</li>';
@@ -1562,7 +1623,7 @@
$('.holiday-preview-popover').remove();
// Create popover HTML
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
var popoverHtml = '<div class="holiday-preview-popover es-preview-popover show">';
popoverHtml += '<div class="popover-header">';
popoverHtml += '<span class="popover-title">';
if (countryIso) {
@@ -1570,10 +1631,10 @@
}
popoverHtml += this.escapeHtml(countryName) + ' - ' + (trans.holidays || 'Holidays');
popoverHtml += '</span>';
popoverHtml += '<button type="button" class="popover-close">' + this.esIcon('close') + '</button>';
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
popoverHtml += '</div>';
popoverHtml += '<div class="popover-body">';
popoverHtml += '<div class="holiday-preview-loading">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '</div>';
popoverHtml += '</div>';
@@ -1660,7 +1721,7 @@
$popover.find('.popover-body').html(listHtml);
} else {
var noDataHtml = '<div class="holiday-preview-empty">';
noDataHtml += self.esIcon('event_busy');
noDataHtml += '<i class="material-icons">event_busy</i>';
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
noDataHtml += '</div>';
$popover.find('.popover-body').html(noDataHtml);
@@ -1677,7 +1738,7 @@
},
error: function() {
var errorHtml = '<div class="holiday-preview-empty">';
errorHtml += self.esIcon('error');
errorHtml += '<i class="material-icons">error_outline</i>';
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
errorHtml += '</div>';
$popover.find('.popover-body').html(errorHtml);
@@ -1703,17 +1764,17 @@
$('.holiday-preview-popover').remove();
// Create popover HTML with placeholder title (will update after AJAX)
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
var popoverHtml = '<div class="holiday-preview-popover es-preview-popover show">';
popoverHtml += '<div class="popover-header">';
popoverHtml += '<span class="popover-title">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</span>';
popoverHtml += '<button type="button" class="popover-close">' + this.esIcon('close') + '</button>';
popoverHtml += '<span class="popover-title"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</span>';
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
popoverHtml += '</div>';
popoverHtml += '<div class="popover-filter">';
popoverHtml += this.esIcon('search');
popoverHtml += '<i class="material-icons">search</i>';
popoverHtml += '<input type="text" class="holiday-filter-input" placeholder="' + (trans.filter_holidays || 'Filter by country, date, name...') + '">';
popoverHtml += '</div>';
popoverHtml += '<div class="popover-body">';
popoverHtml += '<div class="holiday-preview-loading">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '</div>';
popoverHtml += '</div>';
@@ -1901,7 +1962,7 @@
$popover.find('.popover-title').html('0 ' + (trans.holidays || 'Holidays'));
var noDataHtml = '<div class="holiday-preview-empty">';
noDataHtml += self.esIcon('event_busy');
noDataHtml += '<i class="material-icons">event_busy</i>';
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
noDataHtml += '</div>';
$popover.find('.popover-body').html(noDataHtml);
@@ -1918,10 +1979,10 @@
},
error: function() {
// Update header for error state
$popover.find('.popover-title').html(self.esIcon('error') + ' ' + (trans.error || 'Error'));
$popover.find('.popover-title').html('<i class="material-icons">error_outline</i> ' + (trans.error || 'Error'));
var errorHtml = '<div class="holiday-preview-empty">';
errorHtml += self.esIcon('error');
errorHtml += '<i class="material-icons">error_outline</i>';
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
errorHtml += '</div>';
$popover.find('.popover-body').html(errorHtml);

View File

@@ -296,7 +296,7 @@
var selectedIds = [];
var hiddenIds = [];
if (this.activeGroup) {
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
var $block = this.$wrapper.find('.es-block[data-block-type="' + this.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
var currentSearchEntity = this.activeGroup.searchEntity;
var currentExcludeIndex = this.activeGroup.excludeIndex;
@@ -361,7 +361,7 @@
var html = '';
if (visibleResults.length === 0 && !appendMode) {
html = '<div class="no-results">' + this.esIcon('search') + ' ' + (trans.no_results || 'No results found') + '</div>';
html = '<div class="no-results"><i class="icon-search"></i> ' + (trans.no_results || 'No results found') + '</div>';
} else {
visibleResults.forEach(function(item) {
var isSelected = selectedIds.indexOf(String(item.id)) !== -1;
@@ -376,27 +376,27 @@
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
html += '>';
html += '<span class="result-checkbox">' + self.esIcon('check') + '</span>';
html += '<span class="result-checkbox"><i class="icon-check"></i></span>';
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : null;
// Countries show flags
if (searchEntity === 'countries' && item.iso_code) {
var flagUrl = 'https://flagcdn.com/w40/' + item.iso_code.toLowerCase() + '.png';
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;">' + self.esIcon('flag') + '</span></div>';
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;"><i class="icon-flag"></i></span></div>';
} else if (item.image) {
html += '<div class="result-image"><img src="' + self.escapeAttr(item.image) + '" alt=""></div>';
} else {
// Entity-specific icons
var iconName = 'widgets'; // default
if (searchEntity === 'categories') iconName = 'folder';
else if (searchEntity === 'manufacturers') iconName = 'business';
else if (searchEntity === 'suppliers') iconName = 'local_shipping';
else if (searchEntity === 'attributes') iconName = 'brush';
else if (searchEntity === 'features') iconName = 'list';
else if (searchEntity === 'cms') iconName = 'description';
else if (searchEntity === 'cms_categories') iconName = 'folder';
html += '<div class="result-icon">' + self.esIcon(iconName) + '</div>';
var iconClass = 'icon-cube'; // default
if (searchEntity === 'categories') iconClass = 'icon-folder';
else if (searchEntity === 'manufacturers') iconClass = 'icon-building';
else if (searchEntity === 'suppliers') iconClass = 'icon-truck';
else if (searchEntity === 'attributes') iconClass = 'icon-paint-brush';
else if (searchEntity === 'features') iconClass = 'icon-list-ul';
else if (searchEntity === 'cms') iconClass = 'icon-file-text-o';
else if (searchEntity === 'cms_categories') iconClass = 'icon-folder-o';
html += '<div class="result-icon"><i class="' + iconClass + '"></i></div>';
}
html += '<div class="result-info">';
@@ -588,10 +588,10 @@
for (var i = 0; i < history.length; i++) {
var query = history[i];
html += '<div class="history-item" data-query="' + this.escapeAttr(query) + '">';
html += this.esIcon('schedule');
html += '<i class="icon-clock-o"></i>';
html += '<span class="history-query">' + this.escapeHtml(query) + '</span>';
html += '<button type="button" class="btn-delete-history" title="' + (trans.remove || 'Remove') + '">';
html += this.esIcon('close');
html += '<i class="icon-times"></i>';
html += '</button>';
html += '</div>';
}

View File

@@ -36,7 +36,7 @@
var searchEntity = this.activeGroup ? this.activeGroup.searchEntity : 'categories';
// Show loading
$results.html('<div class="tree-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' +
$results.html('<div class="tree-loading"><i class="icon-spinner icon-spin"></i> ' +
this.escapeHtml(trans.loading || 'Loading...') + '</div>');
// Fetch tree data
@@ -115,11 +115,11 @@
html += '<div class="tree-toolbar">';
html += '<button type="button" class="btn-expand-all" title="' +
this.escapeAttr(trans.expand_all || 'Expand all') + '">';
html += this.esIcon('add_box') + ' ' + this.escapeHtml(trans.expand_all || 'Expand all');
html += '<i class="icon-plus-square-o"></i> ' + this.escapeHtml(trans.expand_all || 'Expand all');
html += '</button>';
html += '<button type="button" class="btn-collapse-all" title="' +
this.escapeAttr(trans.collapse_all || 'Collapse all') + '">';
html += this.esIcon('indeterminate_check_box') + ' ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
html += '<i class="icon-minus-square-o"></i> ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
html += '</button>';
html += '</div>';
@@ -178,21 +178,21 @@
// Toggle button (expand/collapse)
if (hasChildren) {
html += '<span class="tree-toggle">' + self.esIcon('arrow_drop_down') + '</span>';
html += '<span class="tree-toggle"><i class="icon-caret-down"></i></span>';
// Select with children button (next to toggle on the left)
html += '<button type="button" class="btn-select-children" title="' +
self.escapeAttr(trans.select_with_children || 'Select with all children') + '">';
html += self.esIcon('check_box');
html += '<i class="icon-check-square-o"></i>';
html += '</button>';
} else {
html += '<span class="tree-toggle tree-leaf"></span>';
}
// Checkbox indicator
html += '<span class="tree-checkbox">' + self.esIcon('check') + '</span>';
html += '<span class="tree-checkbox"><i class="icon-check"></i></span>';
// Category icon
html += '<span class="tree-icon">' + self.esIcon('folder') + '</span>';
html += '<span class="tree-icon"><i class="icon-folder"></i></span>';
// Name
html += '<span class="tree-name">' + self.escapeHtml(node.name) + '</span>';
@@ -203,7 +203,7 @@
var countLabel = node.page_count ? (trans.pages || 'pages') : (trans.products || 'products');
html += '<span class="tree-count clickable" data-category-id="' + node.id + '" ';
html += 'title="' + self.escapeAttr(itemCount + ' ' + countLabel) + '">';
html += self.esIcon('visibility') + ' ' + itemCount;
html += '<i class="icon-eye"></i> ' + itemCount;
html += '</span>';
}
@@ -235,7 +235,7 @@
if (!this.activeGroup) return selectedIds;
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
var $block = this.$wrapper.find('.es-block[data-block-type="' + this.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
var $picker;
@@ -346,10 +346,10 @@
});
if (isParentSelected && allChildrenSelected) {
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
} else {
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
$btn.attr('title', trans.select_with_children || 'Select with all children');
}
});

View File

@@ -19,97 +19,6 @@
// Create mixin namespace
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
// ---------------------------------------------------------------
// Icon framework detection & FA4 mapping (module-level singleton)
// ---------------------------------------------------------------
var _iconMode = null;
/**
* Material Icons → FontAwesome 4 class mapping.
* FA4 uses class-based icons (icon-name), Material uses text content.
*/
var FA4_MAP = {
'account_tree': 'icon-sitemap',
'add': 'icon-plus',
'add_box': 'icon-plus-square',
'arrow_downward': 'icon-sort-desc',
'arrow_drop_down': 'icon-caret-down',
'arrow_right': 'icon-chevron-right',
'arrow_upward': 'icon-sort-asc',
'block': 'icon-ban',
'brush': 'icon-paint-brush',
'business': 'icon-building',
'check': 'icon-check',
'check_box': 'icon-check-square',
'check_box_outline_blank': 'icon-square-o',
'check_circle': 'icon-check-circle',
'close': 'icon-times',
'delete': 'icon-trash',
'description': 'icon-file-text',
'error': 'icon-exclamation-circle',
'event': 'icon-calendar',
'event_busy': 'icon-calendar-times-o',
'expand_less': 'icon-chevron-up',
'expand_more': 'icon-chevron-down',
'filter_list': 'icon-filter',
'flag': 'icon-flag',
'folder': 'icon-folder',
'folder_open': 'icon-folder-open',
'indeterminate_check_box': 'icon-minus-square',
'info': 'icon-info-circle',
'inventory_2': 'icon-archive',
'label': 'icon-tag',
'language': 'icon-globe',
'lightbulb': 'icon-lightbulb-o',
'list': 'icon-list',
'list_alt': 'icon-list-alt',
'local_shipping': 'icon-truck',
'lock': 'icon-lock',
'my_location': 'icon-crosshairs',
'open_in_full': 'icon-expand',
'payments': 'icon-credit-card',
'progress_activity': 'icon-circle-o-notch',
'schedule': 'icon-clock-o',
'search': 'icon-search',
'shopping_cart': 'icon-shopping-cart',
'shuffle': 'icon-random',
'sort': 'icon-sort',
'sort_by_alpha': 'icon-sort-alpha-asc',
'star': 'icon-star',
'sync': 'icon-refresh',
'tune': 'icon-sliders',
'visibility': 'icon-eye',
'warning': 'icon-warning',
'widgets': 'icon-th-large'
};
/**
* Detect icon framework: 'material' (PS 8+/9+) or 'fa4' (PS 1.6/1.7).
* Checks PHP-set data attribute first, falls back to font detection.
*/
function detectIconMode() {
if (_iconMode !== null) return _iconMode;
// 1. PHP sets data-icon-mode on the wrapper
var $w = $('.entity-selector-trait[data-icon-mode], .target-conditions-trait[data-icon-mode]').first();
if ($w.length && $w.data('icon-mode')) {
_iconMode = $w.data('icon-mode');
return _iconMode;
}
// 2. Fallback: probe whether Material Icons font is loaded
var test = document.createElement('i');
test.className = 'material-icons';
test.style.cssText = 'position:absolute;left:-9999px;top:-9999px;font-size:16px;pointer-events:none';
test.textContent = 'check';
(document.body || document.documentElement).appendChild(test);
var family = (window.getComputedStyle(test).fontFamily || '').toLowerCase();
test.parentNode.removeChild(test);
_iconMode = (family.indexOf('material') !== -1) ? 'material' : 'fa4';
return _iconMode;
}
// Utility functions mixin
window._EntitySelectorMixins.utils = {
@@ -151,62 +60,18 @@
.replace(/'/g, '&#039;');
},
/**
* Icon helper — returns HTML for an icon that works on PS 1.6 through 9.x.
* Automatically uses Material Icons (PS 8+/9+) or FontAwesome 4 (PS 1.6/1.7).
*
* @param {string} name - Canonical icon name (Material Icons name, e.g. 'lock', 'search', 'delete')
* @param {string} [extraClass] - Additional CSS class(es) (e.g. 'es-spin', 'method-trigger-icon')
* @returns {string} HTML string for an <i> element
*/
esIcon: function(name, extraClass) {
var mode = detectIconMode();
if (mode === 'material') {
var cls = 'material-icons es-icon';
if (extraClass) cls += ' ' + extraClass;
return '<i class="' + cls + '">' + name + '</i>';
}
// FA4: icon is encoded in the class name, no text content
var mapped = FA4_MAP[name] || 'icon-circle';
var cls = mapped + ' es-icon';
if (extraClass) cls += ' ' + extraClass;
return '<i class="' + cls + '"></i>';
},
/**
* Update an existing <i> icon element to show a different icon.
* Handles both Material Icons and FA4 modes.
*
* @param {jQuery} $el - The <i> element to update
* @param {string} name - Canonical icon name
* @param {string} [extraClass] - Additional CSS class(es) to preserve
*/
esIconUpdate: function($el, name, extraClass) {
var mode = detectIconMode();
if (mode === 'material') {
var cls = 'material-icons es-icon';
if (extraClass) cls += ' ' + extraClass;
$el.attr('class', cls).text(name);
} else {
var mapped = FA4_MAP[name] || 'icon-circle';
var cls = mapped + ' es-icon';
if (extraClass) cls += ' ' + extraClass;
$el.attr('class', cls).text('');
}
},
getEntityTypeIcon: function(entityType) {
var icons = {
'products': 'shopping_cart',
'categories': 'folder_open',
'manufacturers': 'business',
'suppliers': 'local_shipping',
'attributes': 'list_alt',
'features': 'label',
'cms': 'description',
'cms_categories': 'folder'
'products': 'icon-shopping-cart',
'categories': 'icon-folder-open',
'manufacturers': 'icon-building',
'suppliers': 'icon-truck',
'attributes': 'icon-list-alt',
'features': 'icon-tags',
'cms': 'icon-file-text',
'cms_categories': 'icon-folder'
};
return icons[entityType] || 'widgets';
return icons[entityType] || 'icon-cube';
},
getEntityTypeLabel: function(entityType) {
@@ -229,7 +94,7 @@
if (!isRequired) return true;
var hasData = false;
this.$wrapper.find('.target-block').each(function() {
this.$wrapper.find('.es-block').each(function() {
if ($(this).find('.selection-group').length > 0) {
hasData = true;
return false;
@@ -251,12 +116,12 @@
this.$wrapper.find('.trait-validation-error').remove();
var $error = $('<div>', {
class: 'trait-validation-error',
html: this.esIcon('warning') + ' ' + message
html: '<i class="icon-warning"></i> ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
$('html, body').animate({ scrollTop: this.$wrapper.offset().top - 100 }, 300);
if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
this.$wrapper.find('.condition-trait-body').slideDown(200);
if (!this.$wrapper.find('.condition-trait-body').hasClass('es-expanded')) {
this.$wrapper.find('.condition-trait-body').addClass('es-expanded');
this.$wrapper.removeClass('collapsed');
}
},
@@ -275,24 +140,119 @@
return this.getBlockMode(blockType) === 'single';
},
getCurrentSingleSelection: function() {
if ((this.config.mode || 'multi') !== 'single') return null;
var $chip = this.$wrapper.find('.entity-chips .entity-chip').first();
if ($chip.length) {
var $block = $chip.closest('.target-block');
return {
name: $chip.find('.chip-name').text() || $chip.data('id'),
entityType: $block.data('block-type') || 'item'
};
getCurrentSingleSelection: function(blockType) {
// Global single mode — check any chip across all blocks
if ((this.config.mode || 'multi') === 'single') {
var $chip = this.$wrapper.find('.entity-chips .entity-chip').first();
if ($chip.length) {
var $block = $chip.closest('.es-block');
return {
name: $chip.find('.chip-name').text() || $chip.data('id'),
entityType: $block.data('block-type') || 'item'
};
}
return null;
}
// Per-block single mode — check active block or specified blockType
if (blockType) {
if (this.getBlockMode(blockType) !== 'single') return null;
var $block = this.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
var $chip = $block.find('.entity-chips .entity-chip').first();
if ($chip.length) {
return {
name: $chip.find('.chip-name').text() || $chip.data('id'),
entityType: blockType
};
}
}
return null;
},
showReplaceConfirmation: function(currentSelection, newSelection, onConfirm) {
// Close the search dropdown so modal is accessible
if (typeof this.hideDropdown === 'function') {
this.hideDropdown();
}
if (typeof MPRModal === 'undefined') {
if (confirm('Replace "' + currentSelection.name + '" with "' + newSelection.name + '"?')) {
onConfirm();
}
return;
}
var t = this.config.trans || {};
var currentTypeLabel = this.getEntityTypeLabel(currentSelection.entityType);
var newTypeLabel = this.getEntityTypeLabel(newSelection.entityType);
var modal = MPRModal.create({ id: 'mpr-entity-replace-modal' });
modal.setHeader('warning', 'swap_horiz', t.replace_title || 'Replace selection?');
modal.setBody(
'<p class="mpr-replace-message">' + this.escapeHtml(t.replace_message || 'Only one item is allowed. Replace the current selection?') + '</p>' +
'<div class="mpr-replace-item mpr-replace-current">' +
'<div class="mpr-replace-label">' + this.escapeHtml(t.replace_current || 'Current') + '</div>' +
'<div class="mpr-replace-value">' + this.escapeHtml(currentTypeLabel) + ': ' + this.escapeHtml(currentSelection.name) + '</div>' +
'</div>' +
'<div class="mpr-replace-arrow"><i class="material-icons">arrow_downward</i></div>' +
'<div class="mpr-replace-item mpr-replace-new">' +
'<div class="mpr-replace-label">' + this.escapeHtml(t.replace_new || 'New') + '</div>' +
'<div class="mpr-replace-value">' + this.escapeHtml(newTypeLabel) + ': ' + this.escapeHtml(newSelection.name) + '</div>' +
'</div>'
);
modal.setFooter([
{ type: 'cancel', label: t.cancel || 'Cancel' },
{ type: 'primary', label: t.replace || 'Replace', icon: 'check', onClick: function() {
modal.hide();
onConfirm();
}}
]);
modal.show();
},
/**
* Check if entity type supports tree browsing
*/
supportsTreeBrowsing: function(entityType) {
return entityType === 'categories' || entityType === 'cms_categories';
},
/**
* Build a standardized empty state element
* @param {string} text - Message to display
* @param {string} [icon] - Optional icon class (e.g. 'icon-info-circle')
* @returns {string} HTML string
*/
_buildEmptyState: function(text, icon) {
var iconHtml = icon ? '<i class="' + this.escapeAttr(icon) + '"></i> ' : '';
return '<span class="es-empty-state">' + iconHtml + this.escapeHtml(text) + '</span>';
},
/**
* Build a standardized search box (icon + input + spinner)
* @param {string} placeholder - Input placeholder text
* @param {string} [extraClass] - Optional additional CSS class
* @returns {string} HTML string
*/
_buildSearchBoxHtml: function(placeholder, extraClass) {
var cls = extraClass ? ' ' + extraClass : '';
return '<div class="entity-search-box' + cls + '">' +
'<i class="icon-search entity-search-icon"></i>' +
'<input type="text" class="entity-search-input" placeholder="' + this.escapeAttr(placeholder) + '" autocomplete="off">' +
'<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>' +
'</div>';
},
/**
* Build a standardized info tooltip
* @param {string} content - Tooltip content
* @param {string} [type] - 'details' for data-details attr, default uses data-tooltip
* @returns {string} HTML string
*/
_buildInfoTooltip: function(content, type) {
var attr = (type === 'details') ? 'data-details' : 'data-tooltip';
return '<span class="mpr-info-wrapper" ' + attr + '="' + this.escapeAttr(content) + '">' +
'<i class="material-icons">info_outline</i></span>';
}
};

View File

@@ -35,7 +35,7 @@
var trans = this.config.trans || {};
id = parseInt(id, 10);
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
var $block = this.$wrapper.find('.es-block[data-block-type="' + this.activeGroup.blockType + '"]');
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
// Get include chips
@@ -297,12 +297,12 @@
// Create toast HTML
var html = '<div class="es-validation-toast">';
html += '<div class="es-toast-icon">' + this.esIcon('warning') + '</div>';
html += '<div class="es-toast-icon"><i class="icon-exclamation-triangle"></i></div>';
html += '<div class="es-toast-content">';
html += '<div class="es-toast-title">' + this.escapeHtml(title) + '</div>';
html += '<div class="es-toast-message">' + this.escapeHtml(message) + '</div>';
html += '</div>';
html += '<button type="button" class="es-toast-close">' + this.esIcon('close') + '</button>';
html += '<button type="button" class="es-toast-close"><i class="icon-times"></i></button>';
html += '</div>';
var $toast = $(html);

View File

@@ -7,7 +7,6 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Chips container wrapper with toolbar
@@ -332,7 +331,7 @@
color: darken($es-primary, 10%);
}
i {
i.material-icons {
font-size: 14px;
}
}
@@ -343,16 +342,6 @@
word-break: break-word;
}
.chip-attrs {
font-size: 0.85em;
opacity: 0.7;
margin-left: 2px;
&::before {
content: '';
}
}
.chip-remove {
@include button-reset;
display: flex;
@@ -831,7 +820,7 @@
color: $es-text-secondary;
}
i {
i.material-icons {
font-size: 18px;
}
}
@@ -853,7 +842,7 @@
color: $es-text-muted;
font-size: $es-font-size-sm;
i {
i.material-icons {
font-size: 20px;
}
@@ -868,7 +857,7 @@
padding: $es-spacing-xl 0;
color: $es-text-muted;
i {
i.material-icons {
font-size: 48px;
opacity: 0.4;
margin-bottom: $es-spacing-sm;
@@ -978,7 +967,7 @@
border-bottom: 1px solid $es-border-color;
background: $es-slate-50;
i {
i.material-icons {
font-size: 18px;
color: $es-text-muted;
}
@@ -1015,7 +1004,6 @@
#content.bootstrap,
#content .bootstrap,
.bootstrap #content {
.target-conditions-trait,
.entity-selector-trait {
.chips-wrapper .chips-toolbar {
// Double class for extra specificity

View File

@@ -7,7 +7,6 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Main container

View File

@@ -6,16 +6,15 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Search wrapper
.target-search-wrapper {
.es-search-wrapper {
position: relative;
}
// Search dropdown
.target-search-dropdown {
.es-search-dropdown {
@include dropdown-container;
display: none;
width: 600px;
@@ -117,7 +116,7 @@
// Results container
.dropdown-results {
padding: 0 $es-spacing-sm;
padding: 0;
}
// Results count text
@@ -159,12 +158,12 @@
}
// Result item (both class names for compatibility)
// Note: Main dropdown-item styling is in the global .target-search-dropdown section below
// Note: Main dropdown-item styling is in the global .es-search-dropdown section below
.dropdown-result-item {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm 0;
padding: $es-spacing-sm;
background: $es-white;
border: none;
border-bottom: 1px solid $es-border-color;
@@ -359,20 +358,6 @@
gap: $es-spacing-sm;
}
// Combination-level search results ('both' mode)
.dropdown-item.is-combination {
padding-left: 28px;
.result-name {
font-size: 0.9em;
}
}
.dropdown-item.is-parent-product {
background: $es-slate-50;
font-weight: $es-font-weight-medium;
}
// No results state
.no-results {
display: flex;
@@ -616,7 +601,6 @@
}
// Category tree view
.target-conditions-trait,
.entity-selector-trait {
.category-tree {
padding: $es-spacing-sm;
@@ -884,8 +868,8 @@
// Global dropdown styles (when appended to body instead of inside wrapper)
// Duplicates key styles for when dropdown is outside .entity-selector-trait
// =============================================================================
body > .target-search-dropdown,
.target-search-dropdown {
body > .es-search-dropdown,
.es-search-dropdown {
@include dropdown-container;
display: none;
width: 600px;
@@ -1304,9 +1288,50 @@ body > .target-search-dropdown,
font-weight: $es-font-weight-medium;
}
// Count with eye icon (like group-count-badge)
.toggle-count {
display: inline-flex;
align-items: center;
gap: 0.125rem;
color: $es-text-muted;
font-size: 0.65rem;
i {
font-size: 10px;
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;
}
}
}
}
}
@@ -1399,67 +1424,6 @@ body > .target-search-dropdown,
color: rgba(255, 255, 255, 0.8);
}
// Filter chip wrapper (chip/toggle + preview button)
.filter-chip-wrapper {
display: inline-flex;
align-items: stretch;
border-radius: $es-radius-sm;
overflow: hidden;
// Left element gets left-only border-radius
.filter-chip,
.filter-group-toggle {
border-radius: $es-radius-sm 0 0 $es-radius-sm;
}
// Preview eye button — unified for both value chips and group toggles
.chip-preview-btn {
@include button-reset;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 0.375rem;
font-size: 10px;
color: $es-text-muted;
background: $es-slate-100;
border-left: 1px solid $es-border-color;
border-radius: 0 $es-radius-sm $es-radius-sm 0;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: rgba($es-primary, 0.1);
color: $es-primary;
}
&.popover-open {
background: $es-primary;
color: $es-white;
}
&.loading i {
animation: spin 0.6s linear infinite;
}
}
// When no preview button, restore full border-radius
.filter-chip:last-child,
.filter-group-toggle:last-child {
border-radius: $es-radius-sm;
}
// Group toggle active/has-selection states propagate to wrapper border
.filter-group-toggle.active + .chip-preview-btn {
border-left-color: $es-primary;
background: rgba($es-primary, 0.05);
}
.filter-group-toggle.has-selection + .chip-preview-btn {
border-left-color: $es-success;
background: rgba($es-success, 0.03);
}
}
// Dropdown content
.dropdown-content {
max-height: 400px;
@@ -1787,7 +1751,7 @@ body > .target-search-dropdown,
// Results container
.dropdown-results {
padding: 0 $es-spacing-sm;
padding: 0;
background: $es-white;
min-height: 200px;
}
@@ -1798,7 +1762,7 @@ body > .target-search-dropdown,
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm 0;
padding: $es-spacing-sm;
background: $es-white;
border: none;
border-bottom: 1px solid $es-border-color;
@@ -2295,15 +2259,15 @@ body > .target-search-dropdown,
// ============================================================================
// Standalone dropdown styles (for when dropdown is appended to body)
// These selectors work because .target-search-dropdown is on the dropdown itself
// These selectors work because .es-search-dropdown is on the dropdown itself
// ============================================================================
.target-search-dropdown {
.es-search-dropdown {
// Results container - scrollable
.dropdown-results {
max-height: 400px;
overflow-y: auto;
padding: 0 $es-spacing-sm;
padding: 0;
@include custom-scrollbar;
}
@@ -2385,7 +2349,7 @@ body > .target-search-dropdown,
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: 0;
padding: $es-spacing-sm;
border: none;
border-bottom: 1px solid $es-border-color;
border-radius: 0;
@@ -2559,7 +2523,7 @@ body > .target-search-dropdown,
}
// Body-level dropdown (when appended to body for z-index)
body > .target-search-dropdown {
body > .es-search-dropdown {
// Override dropdown-item border when inside body-appended dropdown
.dropdown-item {
border: none;

View File

@@ -6,8 +6,7 @@
@use '../variables' as *;
@use '../mixins' as *;
// Main wrapper (supports both .target-conditions-trait and .entity-selector-trait)
.target-conditions-trait,
// Main wrapper (supports both .entity-selector-trait and .entity-selector-trait)
.entity-selector-trait {
position: relative;
overflow: visible;
@@ -174,7 +173,7 @@
}
// Block type tabs
.target-block-tabs {
.es-block-tabs {
display: flex;
flex-wrap: wrap;
gap: 0;
@@ -183,7 +182,7 @@
border-bottom: 1px solid $es-border-color;
}
.target-block-tab {
.es-block-tab {
position: relative;
display: flex;
align-items: center;
@@ -237,7 +236,7 @@
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
.target-block-tabs {
.es-block-tabs {
flex: 1;
border-bottom: 0;
border-radius: $es-radius-lg 0 0 0;
@@ -261,7 +260,7 @@
color: $es-primary;
}
> i {
.material-icons {
font-size: 20px !important;
}
}
@@ -279,7 +278,7 @@
}
// Block container
.target-block-container {
.es-block-container {
display: none;
&.active {
@@ -287,18 +286,18 @@
}
}
.target-block-content {
.es-block-content {
padding: $es-spacing-md;
}
.target-block-groups {
.es-block-groups {
display: flex;
flex-direction: column;
gap: $es-spacing-md;
}
// Block header (for standalone blocks)
.target-block-header {
.es-block-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -308,7 +307,7 @@
}
// Empty state
.target-block-empty {
.es-block-empty {
display: flex;
flex-direction: column;
align-items: center;
@@ -366,25 +365,18 @@
}
// Single mode specific styles
.target-conditions-trait.single-mode,
.entity-selector-trait.single-mode,
.entity-selector-trait.single-mode {
// Hide tabs in standalone layout (has separate header, 1 tab is redundant)
.target-block-tabs {
.es-block-tabs {
display: none;
}
.target-block-container {
.es-block-container {
display: block;
}
// In form-content layout, always show tabs — they serve as the block title
.entity-selector-tabs-row .target-block-tabs {
display: flex;
}
}
// Header action buttons
.target-conditions-trait,
.entity-selector-trait {
.header-actions {
display: flex;

View File

@@ -6,11 +6,10 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Group container
.target-group {
.es-group {
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
@@ -18,7 +17,7 @@
}
// Group header
.target-group-header {
.es-group-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -28,7 +27,7 @@
border-bottom: 1px solid $es-border-color;
}
.target-group-title {
.es-group-title {
display: flex;
align-items: center;
gap: $es-spacing-sm;
@@ -51,7 +50,7 @@
}
}
.target-group-actions {
.es-group-actions {
display: flex;
align-items: center;
gap: $es-spacing-xs;
@@ -80,7 +79,7 @@
}
// Group body
.target-group-body,
.es-group-body,
.group-body {
padding: $es-spacing-md;
}
@@ -931,36 +930,6 @@
background-size: 1.25em 1.25em;
}
// Single mode — strip padding, borders, backgrounds for clean single-selection UI
&[data-mode=single],
.mode-single {
.groups-container {
padding: 0;
}
.group-body {
padding: 0;
}
.group-include {
padding: 0.5rem;
margin-bottom: 0;
background: transparent;
border: none;
border-radius: 0;
}
.selection-group {
background: transparent;
border: none;
margin-bottom: 0;
}
.group-header {
display: none;
}
}
// Condition match count badge
.condition-match-count {
display: inline-flex;

View File

@@ -13,8 +13,8 @@
// Preview Popover Container
// =============================================================================
.target-preview-popover,
.target-list-preview-popover {
.es-preview-popover,
.es-list-preview-popover {
position: absolute;
z-index: 10000;
min-width: 320px;
@@ -66,23 +66,6 @@
transform: none;
}
}
// Positioned above trigger - arrow pointing down
&.position-above {
&::before {
top: auto;
bottom: -8px;
border-top: 8px solid $es-border-color;
border-bottom: 0;
}
&::after {
top: auto;
bottom: -6px;
border-top: 6px solid $es-white;
border-bottom: 0;
}
}
}
// =============================================================================
@@ -579,6 +562,7 @@
}
// Icon styles
> .material-icons,
> i:first-child {
flex-shrink: 0;
width: 16px;
@@ -638,7 +622,7 @@
opacity: 0.8;
}
> i {
.material-icons {
font-size: 12px;
line-height: 1;
}

View File

@@ -6,7 +6,6 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Method dropdown trigger button
@@ -14,8 +13,8 @@
display: inline-flex;
align-items: center;
gap: 0.5rem;
height: 36px;
padding: 0 $es-spacing-md;
min-height: 36px;
padding: 0.25rem $es-spacing-md;
border-radius: $es-radius-md;
background: $es-white;
color: $es-slate-800;
@@ -53,6 +52,7 @@
overflow: hidden;
text-overflow: ellipsis;
font-weight: $es-font-weight-medium;
line-height: 1.4;
}
.method-trigger-caret {

View File

@@ -1,488 +1,2 @@
/**
* Modal Component
* Preview modals, confirmation dialogs
*/
@use "sass:color";
@use '../variables' as *;
@use '../mixins' as *;
// Modal backdrop
.mpr-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: $es-z-modal;
opacity: 0;
transition: opacity $es-transition-normal;
&.show {
opacity: 1;
}
}
// Modal container (exclude Bootstrap .modal to prevent collision)
.mpr-modal:not(.modal) {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
z-index: $es-z-modal + 1;
width: 90%;
max-width: 600px;
max-height: 90vh;
background: $es-white;
border-radius: $es-radius-xl;
box-shadow: $es-shadow-xl;
opacity: 0;
transition: all $es-transition-normal;
overflow: hidden;
display: flex;
flex-direction: column;
&.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
&.modal-sm {
max-width: 400px;
}
&.modal-lg {
max-width: 800px;
}
&.modal-xl {
max-width: 1000px;
}
&.modal-fullscreen {
width: 95%;
max-width: none;
height: 90vh;
max-height: none;
}
}
// Modal header
.mpr-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-md;
padding: $es-spacing-md $es-spacing-lg;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
flex-shrink: 0;
}
.mpr-modal-title {
font-size: $es-font-size-base;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
margin: 0;
}
.mpr-modal-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: $es-text-muted;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i {
font-size: $es-font-size-lg;
}
}
// Modal body
.mpr-modal-body {
flex: 1;
overflow-y: auto;
padding: $es-spacing-lg;
@include custom-scrollbar;
}
// Modal footer
.mpr-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: $es-spacing-sm;
padding: $es-spacing-md $es-spacing-lg;
background: $es-bg-header;
border-top: 1px solid $es-border-color;
flex-shrink: 0;
}
.mpr-modal-btn {
@include button-reset;
display: inline-flex;
align-items: center;
justify-content: center;
gap: $es-spacing-xs;
padding: $es-spacing-sm $es-spacing-md;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&.btn-secondary {
color: $es-text-secondary;
background: $es-slate-100;
&:hover {
background: $es-slate-200;
}
}
&.btn-primary {
color: $es-white;
background: $es-primary;
&:hover {
background: $es-primary-hover;
}
}
&.btn-danger {
color: $es-white;
background: $es-danger;
&:hover {
background: color.adjust($es-danger, $lightness: -10%);
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Preview popover styles moved to _list-preview.scss
.popover-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
}
.popover-title {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.popover-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
}
.popover-body {
max-height: 300px;
overflow-y: auto;
padding: $es-spacing-sm;
@include custom-scrollbar;
}
.popover-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-top: 1px solid $es-border-color;
border-radius: 0 0 $es-radius-lg $es-radius-lg;
}
.popover-info {
font-size: $es-font-size-xs;
color: $es-text-muted;
}
.popover-load-more {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-primary;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
}
// Popover arrow
.popover-arrow {
position: absolute;
width: 12px;
height: 12px;
background: $es-white;
border: 1px solid $es-border-color;
transform: rotate(45deg);
&.arrow-top {
top: -7px;
left: 50%;
margin-left: -6px;
border-right: none;
border-bottom: none;
}
&.arrow-bottom {
bottom: -7px;
left: 50%;
margin-left: -6px;
border-left: none;
border-top: none;
}
}
// ==========================================================================
// Holiday Preview Modal
// ==========================================================================
#mpr-holiday-preview-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $es-z-modal;
&.show {
display: flex;
align-items: center;
justify-content: center;
}
.mpr-modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.mpr-modal-dialog {
position: relative;
width: 90%;
max-width: 480px;
max-height: 80vh;
background: $es-white;
border-radius: $es-radius-lg;
box-shadow: $es-shadow-xl;
display: flex;
flex-direction: column;
overflow: hidden;
}
.mpr-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-md;
padding: $es-spacing-md $es-spacing-lg;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
flex-shrink: 0;
}
.mpr-modal-title {
display: flex;
align-items: center;
gap: $es-spacing-sm;
font-size: $es-font-size-base;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
margin: 0;
i> i {
font-size: 20px;
color: $es-primary;
}
}
.mpr-modal-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: $es-text-muted;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i {
font-size: 18px;
}
}
.mpr-modal-body {
flex: 1;
overflow-y: auto;
padding: $es-spacing-lg;
@include custom-scrollbar;
}
// Loading state
.holiday-preview-loading {
display: flex;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-xl 0;
color: $es-text-muted;
font-size: $es-font-size-sm;
i {
font-size: $es-font-size-lg;
}
}
// Empty state
.holiday-preview-empty {
text-align: center;
padding: $es-spacing-xl 0;
color: $es-text-muted;
i> i {
font-size: 48px;
opacity: 0.5;
margin-bottom: $es-spacing-md;
}
p {
margin: 0 0 $es-spacing-xs;
}
.hint {
font-size: $es-font-size-xs;
color: $es-text-muted;
}
}
// Holiday list
.holiday-list {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.holiday-item {
display: flex;
align-items: flex-start;
gap: $es-spacing-md;
padding: $es-spacing-sm $es-spacing-md;
background: $es-slate-50;
border-radius: $es-radius-md;
border-left: 3px solid $es-success;
&.holiday-type-bank {
border-left-color: $es-info;
}
&.holiday-type-observance {
border-left-color: $es-warning;
}
&.holiday-type-regional {
border-left-color: #8b5cf6;
}
}
.holiday-date {
flex-shrink: 0;
min-width: 100px;
.holiday-day {
display: block;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.holiday-weekday {
display: block;
font-size: $es-font-size-xs;
color: $es-text-muted;
}
}
.holiday-info {
flex: 1;
min-width: 0;
}
.holiday-name {
display: block;
font-size: $es-font-size-sm;
color: $es-text-primary;
word-wrap: break-word;
}
.holiday-type-badge {
display: inline-block;
margin-top: $es-spacing-xs;
padding: 0.125rem 0.375rem;
font-size: 10px;
font-weight: $es-font-weight-medium;
text-transform: capitalize;
background: $es-slate-200;
color: $es-text-secondary;
border-radius: $es-radius-sm;
}
.holiday-preview-note {
margin-top: $es-spacing-md;
font-size: $es-font-size-xs;
color: $es-text-muted;
text-align: center;
}
}
// Modal component removed - was dead code conflicting with Bootstrap .modal
// Modal functionality is provided by prestashop-admin package (mpr-admin-modal-* classes)

View File

@@ -0,0 +1,76 @@
// Replace confirmation modal for single-mode entity selectors
// Displayed when user tries to replace an already-selected item
@use '../variables' as *;
.mpr-replace-body {
padding: $es-spacing-sm 0;
}
.mpr-replace-message {
margin: 0 0 $es-spacing-md;
color: $es-text-secondary;
font-size: $es-font-size-sm;
line-height: $es-line-height-normal;
}
.mpr-replace-item {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
border-radius: $es-radius-md;
border: 1px solid $es-border-color;
background: $es-white;
}
.mpr-replace-current {
border-color: $es-danger;
background: $es-danger-light;
.mpr-replace-label {
color: $es-danger-dark;
background: rgba($es-danger, 0.12);
}
}
.mpr-replace-new {
border-color: $es-success;
background: $es-success-light;
.mpr-replace-label {
color: $es-success-dark;
background: rgba($es-success, 0.12);
}
}
.mpr-replace-label {
flex-shrink: 0;
padding: 2px $es-spacing-sm;
border-radius: $es-radius-sm;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.mpr-replace-value {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
color: $es-text-primary;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.mpr-replace-arrow {
display: flex;
justify-content: center;
padding: $es-spacing-xs 0;
color: $es-text-light;
.material-icons {
font-size: 20px;
}
}

View File

@@ -329,7 +329,7 @@
background: $es-slate-200;
}
> i {
.material-icons {
color: $es-slate-400;
font-size: 20px;
}
@@ -357,7 +357,7 @@
border-radius: $es-radius-full;
white-space: nowrap;
> i {
.material-icons {
font-size: 14px;
opacity: 0.7;
}

View File

@@ -6,11 +6,10 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Tips box container
.target-tips-box {
.es-tips-box {
margin: $es-spacing-lg $es-spacing-md $es-spacing-md;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
@@ -55,7 +54,7 @@
}
// Expanded state
.target-tips-box.expanded {
.es-tips-box.expanded {
.tips-toggle {
transform: rotate(180deg);
}

View File

@@ -17,13 +17,13 @@
vertical-align: middle;
margin-left: 0.25rem;
> i {
font-size: 14px;
.material-icons {
font-size: 16px;
color: $es-text-muted;
transition: color 0.15s ease;
}
&:hover > i {
&:hover .material-icons {
color: $es-primary;
}
}
@@ -92,7 +92,7 @@
line-height: 1;
transition: background-color 0.15s ease;
> i {
.material-icons {
font-size: 16px;
color: $es-text-muted;
}
@@ -100,7 +100,7 @@
&:hover {
background: $es-slate-100;
> i {
.material-icons {
color: $es-slate-700;
}
}

View File

@@ -324,7 +324,7 @@
}
// Tree view mode in dropdown
.target-search-dropdown.view-tree {
.es-search-dropdown.view-tree {
.dropdown-results {
padding: 0;
}

View File

@@ -6,7 +6,6 @@
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Value picker container

View File

@@ -6,14 +6,12 @@
@use '../variables' as *;
// Base border reset for all entity-selector elements
.target-conditions-trait,
.target-conditions-trait *,
.entity-selector-trait,
.entity-selector-trait *,
.method-dropdown-menu,
.method-dropdown-menu *,
.target-preview-popover,
.target-preview-popover * {
.es-preview-popover,
.es-preview-popover * {
border-style: solid;
border-width: 0;
border-color: $es-border-color;
@@ -22,7 +20,7 @@
// Full-width form group override using :has()
// Excludes .layout-form-group which uses standard PrestaShop form layout
.form-group:has(.entity-selector-trait:not(.layout-form-group)),
.form-group:has(.target-conditions-trait:not(.layout-form-group)),
.form-group:has(.entity-selector-trait:not(.layout-form-group)),
.form-group:has(.condition-trait:not(.layout-form-group)) {
display: block;
@@ -65,26 +63,26 @@
// Dropdown overflow fix
// When dropdown is open, parent containers must allow overflow
.panel:has(.target-search-dropdown.show),
.card:has(.target-search-dropdown.show),
.form-wrapper:has(.target-search-dropdown.show),
.panel-body:has(.target-search-dropdown.show),
.card-body:has(.target-search-dropdown.show),
.form-group:has(.target-search-dropdown.show),
.col-lg-8:has(.target-search-dropdown.show),
.col-lg-12:has(.target-search-dropdown.show) {
.panel:has(.es-search-dropdown.show),
.card:has(.es-search-dropdown.show),
.form-wrapper:has(.es-search-dropdown.show),
.panel-body:has(.es-search-dropdown.show),
.card-body:has(.es-search-dropdown.show),
.form-group:has(.es-search-dropdown.show),
.col-lg-8:has(.es-search-dropdown.show),
.col-lg-12:has(.es-search-dropdown.show) {
overflow: visible !important;
}
// Target conditions wrapper hierarchy overflow fix
.target-conditions-trait:has(.target-search-dropdown.show),
.entity-selector-trait:has(.target-search-dropdown.show),
.condition-trait-body:has(.target-search-dropdown.show),
.target-block-content:has(.target-search-dropdown.show),
.target-block-groups:has(.target-search-dropdown.show),
.target-group:has(.target-search-dropdown.show),
.target-group-body:has(.target-search-dropdown.show),
.target-search-wrapper:has(.target-search-dropdown.show) {
.entity-selector-trait:has(.es-search-dropdown.show),
.entity-selector-trait:has(.es-search-dropdown.show),
.condition-trait-body:has(.es-search-dropdown.show),
.es-block-content:has(.es-search-dropdown.show),
.es-block-groups:has(.es-search-dropdown.show),
.es-group:has(.es-search-dropdown.show),
.es-group-body:has(.es-search-dropdown.show),
.es-search-wrapper:has(.es-search-dropdown.show) {
overflow: visible !important;
}
@@ -94,7 +92,7 @@
// Use .layout-embedded for entity selectors nested inside other components
// Removes outer wrapper styling to avoid redundant borders/backgrounds
.target-conditions-trait.layout-embedded,
.entity-selector-trait.layout-embedded,
.entity-selector-trait.layout-embedded {
background: transparent;
border: none;

View File

@@ -7,7 +7,6 @@
// Tablet and below
@media (max-width: 991px) {
.target-conditions-trait,
.entity-selector-trait {
.condition-trait-header {
flex-direction: column;
@@ -20,7 +19,7 @@
justify-content: flex-end;
}
.target-block-tabs {
.es-block-tabs {
flex-wrap: wrap;
}
}
@@ -28,19 +27,18 @@
// Mobile
@media (max-width: 767px) {
.target-conditions-trait,
.entity-selector-trait {
.target-block-tab {
.es-block-tab {
padding: $es-spacing-sm;
font-size: $es-font-size-xs;
}
.target-group-header {
.es-group-header {
flex-direction: column;
align-items: flex-start;
}
.target-search-dropdown {
.es-search-dropdown {
width: 100% !important;
left: 0 !important;
right: 0 !important;
@@ -54,7 +52,6 @@
// High-resolution displays
@media (min-width: 1600px) {
.target-conditions-trait,
.entity-selector-trait {
.dropdown-results-grid.view-grid-3 {
grid-template-columns: repeat(4, 1fr);

View File

@@ -31,3 +31,44 @@
@use 'components/tooltip';
@use 'components/tree';
@use 'components/validation';
@use 'components/replace-modal';
// Refactor additions (Mar 2026)
// Loading state
.loading-count { opacity: 0.5; transition: opacity 0.15s; }
.tab-badge.loading { opacity: 0.5; transition: opacity 0.15s; }
// Expand/collapse CSS transitions (replaces jQuery slideDown/slideUp)
.entity-selector-blocks-content,
.condition-trait-body,
.group-modifiers-content {
transition: max-height 0.2s ease-out, opacity 0.2s ease-out;
overflow: hidden;
}
.entity-selector-blocks-content:not(.es-expanded),
.condition-trait-body:not(.es-expanded),
.group-modifiers-content:not(.es-expanded) {
max-height: 0 !important;
opacity: 0;
pointer-events: none;
}
.entity-selector-blocks-content.es-expanded,
.condition-trait-body.es-expanded,
.group-modifiers-content.es-expanded {
max-height: 2000px;
opacity: 1;
pointer-events: auto;
}
// Empty state component
.es-empty-state,
.chips-empty-state {
display: block;
padding: 0.75rem 1rem;
color: #94a3b8;
font-size: 0.8rem;
font-style: italic;
text-align: center;
}
// chips-wrapper, chips-toolbar, and chips-load-more are created by JS
// only when chips exist — not rendered in template when empty

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ use Configuration;
use Tools;
use ImageType;
use Shop;
use MyPrestaRocks\Search\SearchEngine;
class EntitySearchEngine
{
@@ -34,6 +35,16 @@ class EntitySearchEngine
*/
protected $idShop;
/**
* @var \MyPrestaRocks\Search\Tokenizer|null Lazy-loaded tokenizer
*/
protected $tokenizer;
/**
* @var \MyPrestaRocks\Search\WordVariants|null Lazy-loaded word variants
*/
protected $wordVariants;
/**
* Constructor
*
@@ -142,6 +153,99 @@ class EntitySearchEngine
return str_replace(['%', '_'], ['\\%', '\\_'], pSQL($pattern));
}
/**
* Build smart LIKE conditions for a search query against one or more columns.
* Uses WordVariants for stemming so "shower" also matches "showers", etc.
* Falls back to simple LIKE if prestashop-search is not available.
*
* @param string $query Search query
* @param string[] $columns SQL columns to match (e.g. ['pl.name', 'p.reference'])
* @return string SQL WHERE fragment (without leading AND/OR), or empty string
*/
protected function buildSmartSearch($query, array $columns)
{
$query = trim($query);
if ($query === '' || empty($columns)) {
return '';
}
if (!class_exists('MyPrestaRocks\\Search\\Tokenizer')) {
// Fallback: simple LIKE
$escaped = $this->escapePattern($query);
$likes = [];
foreach ($columns as $col) {
$likes[] = $col . " LIKE '%" . $escaped . "%'";
}
return '(' . implode(' OR ', $likes) . ')';
}
if ($this->tokenizer === null) {
$this->tokenizer = new \MyPrestaRocks\Search\Tokenizer();
$langIso = Context::getContext()->language->iso_code ?? null;
$this->wordVariants = new \MyPrestaRocks\Search\WordVariants($langIso);
}
$parsed = $this->tokenizer->parse($query);
$words = $parsed['words'];
$dimensions = isset($parsed['dimensions']) ? $parsed['dimensions'] : [];
if (empty($words) && empty($dimensions)) {
return '';
}
$wordGroups = [];
// Regular word groups (AND between words, OR between variants)
foreach ($words as $word) {
if (mb_strlen($word) < 2) {
continue;
}
$variants = $this->wordVariants->getVariants($word);
$variants = array_unique($variants);
$variantConditions = [];
foreach ($variants as $variant) {
$escaped = pSQL($variant);
foreach ($columns as $col) {
$variantConditions[] = $col . " LIKE '%" . $escaped . "%'";
}
}
if (!empty($variantConditions)) {
$wordGroups[] = '(' . implode(' OR ', $variantConditions) . ')';
}
}
// Dimension groups — generate LIKE conditions for all variants
if (!empty($dimensions) && class_exists('MyPrestaRocks\\Search\\DimensionHandler')) {
$dimHandler = new \MyPrestaRocks\Search\DimensionHandler();
foreach ($dimensions as $dim) {
$dimVariants = $dimHandler->getVariants($dim);
$dimConditions = [];
foreach ($dimVariants as $variant => $quality) {
if ($quality < 0.3) {
continue;
}
$escaped = pSQL($variant);
foreach ($columns as $col) {
$dimConditions[] = $col . " LIKE '%" . $escaped . "%'";
}
}
if (!empty($dimConditions)) {
$wordGroups[] = '(' . implode(' OR ', $dimConditions) . ')';
}
}
}
if (empty($wordGroups)) {
return '';
}
// All groups must match (AND between word groups and dimension groups)
return '(' . implode(' AND ', $wordGroups) . ')';
}
/**
* Build ORDER BY clause based on entity type and sort field
*
@@ -207,10 +311,32 @@ class EntitySearchEngine
$sql->leftJoin('stock_available', 'sa', 'sa.id_product = p.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . (int) $idShop);
$sql->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
// Search query
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
// Search query — use shared SearchEngine when available, fall back to LIKE
$compiled = null;
$hasSearchEngine = class_exists('MyPrestaRocks\\Search\\SearchEngine');
if (!empty($query) && $hasSearchEngine) {
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
'fuzzy_enabled' => true,
'variants_enabled' => true,
'search_attributes' => false,
'search_features' => false,
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
]);
$compiled = $searchEngine->compile($query);
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
if (!empty($whereClause)) {
$sql->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
$sql->select('(' . $compiled['score'] . ') AS relevance_score');
} else {
$sql->where('p.id_product = ' . (int) $query);
$sql->select('0 AS relevance_score');
}
} elseif (!empty($query)) {
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
$sql->select('0 AS relevance_score');
} else {
$sql->select('0 AS relevance_score');
}
// Apply filters
@@ -235,8 +361,14 @@ class EntitySearchEngine
'reference' => 'p.reference',
'popularity' => 'sales_qty',
'stock' => 'stock_qty',
'relevance' => 'relevance_score',
];
$sql->orderBy($this->buildOrderBy('products', $filters, $productSortMap));
// Default to relevance sort when searching
if (!empty($query) && empty($filters['sort_by'])) {
$sql->orderBy('relevance_score DESC, pl.name ASC');
} else {
$sql->orderBy($this->buildOrderBy('products', $filters, $productSortMap));
}
$sql->limit((int) $limit, (int) $offset);
$results = Db::getInstance()->executeS($sql);
@@ -313,9 +445,24 @@ class EntitySearchEngine
$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);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
if (!empty($query) && class_exists('MyPrestaRocks\\Search\\SearchEngine')) {
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
'fuzzy_enabled' => true,
'variants_enabled' => true,
'search_attributes' => false,
'search_features' => false,
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
]);
$compiled = $searchEngine->compile($query);
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
if (!empty($whereClause)) {
$sql->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
} else {
$sql->where('p.id_product = ' . (int) $query);
}
} elseif (!empty($query)) {
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
}
$this->applyProductFilters($sql, $filters, $idLang, $idShop);
@@ -450,7 +597,6 @@ class EntitySearchEngine
public function searchProductCombinations($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$db = Db::getInstance();
$escapedQuery = !empty($query) ? $this->escapePattern($query) : '';
// Step 1: Find matching products (same query as searchTargetProducts but no limit — we paginate the flat result)
$sqlProducts = new DbQuery();
@@ -469,12 +615,29 @@ class EntitySearchEngine
$sqlProducts->leftJoin('stock_available', 'sa', 'sa.id_product = p.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . (int) $idShop);
$sqlProducts->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
if (!empty($escapedQuery)) {
$sqlProducts->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
if (!empty($query) && class_exists('MyPrestaRocks\\Search\\SearchEngine')) {
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
'fuzzy_enabled' => true,
'variants_enabled' => true,
'search_attributes' => false,
'search_features' => false,
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
]);
$compiled = $searchEngine->compile($query);
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
if (!empty($whereClause)) {
$sqlProducts->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
$sqlProducts->select('(' . $compiled['score'] . ') AS relevance_score');
} else {
$sqlProducts->where('p.id_product = ' . (int) $query);
}
} elseif (!empty($query)) {
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
$sqlProducts->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
}
$this->applyProductFilters($sqlProducts, $filters, $idLang, $idShop);
$sqlProducts->orderBy('pl.name ASC');
$sqlProducts->orderBy(!empty($query) && !empty($compiled) ? 'relevance_score DESC, pl.name ASC' : 'pl.name ASC');
$matchingProducts = $db->executeS($sqlProducts);
if (!$matchingProducts) {
@@ -686,7 +849,6 @@ class EntitySearchEngine
public function countProductCombinations($query, $idLang, $idShop, array $filters = [])
{
$db = Db::getInstance();
$escapedQuery = !empty($query) ? $this->escapePattern($query) : '';
// Get matching product IDs
$sqlProducts = new DbQuery();
@@ -695,8 +857,24 @@ class EntitySearchEngine
$sqlProducts->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
$sqlProducts->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
if (!empty($escapedQuery)) {
$sqlProducts->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
if (!empty($query) && class_exists('MyPrestaRocks\\Search\\SearchEngine')) {
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
'fuzzy_enabled' => true,
'variants_enabled' => true,
'search_attributes' => false,
'search_features' => false,
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
]);
$compiled = $searchEngine->compile($query);
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
if (!empty($whereClause)) {
$sqlProducts->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
} else {
$sqlProducts->where('p.id_product = ' . (int) $query);
}
} elseif (!empty($query)) {
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
$sqlProducts->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
}
$this->applyProductFilters($sqlProducts, $filters, $idLang, $idShop);
@@ -1080,8 +1258,8 @@ class EntitySearchEngine
$sql->where('c.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.id_category = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_category = ' . (int) $query . ')');
}
// Refine query
@@ -1174,8 +1352,8 @@ class EntitySearchEngine
$sql->where('c.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.id_category = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_category = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1429,8 +1607,8 @@ class EntitySearchEngine
$sql->innerJoin('manufacturer_shop', 'ms', 'ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(m.name LIKE \'%' . $escapedQuery . '%\' OR m.id_manufacturer = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['m.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR m.id_manufacturer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1499,8 +1677,8 @@ class EntitySearchEngine
$sql->innerJoin('manufacturer_shop', 'ms', 'ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(m.name LIKE \'%' . $escapedQuery . '%\' OR m.id_manufacturer = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['m.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR m.id_manufacturer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1577,8 +1755,8 @@ class EntitySearchEngine
$sql->innerJoin('supplier_shop', 'ss', 'ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_supplier = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_supplier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1631,8 +1809,8 @@ class EntitySearchEngine
$sql->innerJoin('supplier_shop', 'ss', 'ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_supplier = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_supplier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1709,8 +1887,8 @@ class EntitySearchEngine
$sql->leftJoin('cms_category_lang', 'ccl', 'ccl.id_cms_category = c.id_cms_category AND ccl.id_lang = ' . (int) $idLang . ' AND ccl.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.meta_title LIKE \'%' . $escapedQuery . '%\' OR c.id_cms = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.meta_title']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_cms = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1765,8 +1943,8 @@ class EntitySearchEngine
$sql->leftJoin('cms_lang', 'cl', 'cl.id_cms = c.id_cms AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.meta_title LIKE \'%' . $escapedQuery . '%\' OR c.id_cms = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.meta_title']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_cms = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1845,8 +2023,8 @@ class EntitySearchEngine
$sql->where('cc.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(ccl.name LIKE \'%' . $escapedQuery . '%\' OR cc.id_cms_category = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['ccl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR cc.id_cms_category = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1902,8 +2080,8 @@ class EntitySearchEngine
$sql->where('cc.id_parent > 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(ccl.name LIKE \'%' . $escapedQuery . '%\' OR cc.id_cms_category = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['ccl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR cc.id_cms_category = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -1981,8 +2159,8 @@ class EntitySearchEngine
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = e.id_profile AND pl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(e.firstname LIKE \'%' . $escapedQuery . '%\' OR e.lastname LIKE \'%' . $escapedQuery . '%\' OR e.email LIKE \'%' . $escapedQuery . '%\' OR e.id_employee = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['e.firstname', 'e.lastname', 'e.email']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR e.id_employee = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2034,8 +2212,8 @@ class EntitySearchEngine
$sql->from('employee', 'e');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(e.firstname LIKE \'%' . $escapedQuery . '%\' OR e.lastname LIKE \'%' . $escapedQuery . '%\' OR e.email LIKE \'%' . $escapedQuery . '%\' OR e.id_employee = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['e.firstname', 'e.lastname', 'e.email']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR e.id_employee = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2113,8 +2291,8 @@ class EntitySearchEngine
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.firstname LIKE \'%' . $escapedQuery . '%\' OR c.lastname LIKE \'%' . $escapedQuery . '%\' OR c.email LIKE \'%' . $escapedQuery . '%\' OR c.company LIKE \'%' . $escapedQuery . '%\' OR c.id_customer = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['c.firstname', 'c.lastname', 'c.email', 'c.company']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_customer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2171,8 +2349,8 @@ class EntitySearchEngine
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.firstname LIKE \'%' . $escapedQuery . '%\' OR c.lastname LIKE \'%' . $escapedQuery . '%\' OR c.email LIKE \'%' . $escapedQuery . '%\' OR c.company LIKE \'%' . $escapedQuery . '%\' OR c.id_customer = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['c.firstname', 'c.lastname', 'c.email', 'c.company']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_customer = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2251,8 +2429,8 @@ class EntitySearchEngine
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(gl.name LIKE \'%' . $escapedQuery . '%\' OR g.id_group = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['gl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR g.id_group = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2303,8 +2481,8 @@ class EntitySearchEngine
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(gl.name LIKE \'%' . $escapedQuery . '%\' OR g.id_group = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['gl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR g.id_group = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2376,8 +2554,8 @@ class EntitySearchEngine
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.name LIKE \'%' . $escapedQuery . '%\' OR c.id_carrier = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['c.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_carrier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2432,8 +2610,8 @@ class EntitySearchEngine
$sql->where('c.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(c.name LIKE \'%' . $escapedQuery . '%\' OR c.id_carrier = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['c.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_carrier = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2509,8 +2687,8 @@ class EntitySearchEngine
$sql->innerJoin('zone_shop', 'zs', 'zs.id_zone = z.id_zone AND zs.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(z.name LIKE \'%' . $escapedQuery . '%\' OR z.id_zone = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['z.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR z.id_zone = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2561,8 +2739,8 @@ class EntitySearchEngine
$sql->innerJoin('zone_shop', 'zs', 'zs.id_zone = z.id_zone AND zs.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(z.name LIKE \'%' . $escapedQuery . '%\' OR z.id_zone = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['z.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR z.id_zone = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2640,8 +2818,8 @@ class EntitySearchEngine
$sql->leftJoin('zone', 'z', 'z.id_zone = c.id_zone');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_country = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_country = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2722,8 +2900,8 @@ class EntitySearchEngine
$sql->leftJoin('country_lang', 'cl', 'cl.id_country = c.id_country AND cl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_country = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_country = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2824,8 +3002,8 @@ class EntitySearchEngine
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_currency = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_currency = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2882,8 +3060,8 @@ class EntitySearchEngine
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_currency = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_currency = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -2962,8 +3140,8 @@ class EntitySearchEngine
$sql->innerJoin('lang_shop', 'ls', 'ls.id_lang = l.id_lang AND ls.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(l.name LIKE \'%' . $escapedQuery . '%\' OR l.iso_code LIKE \'%' . $escapedQuery . '%\' OR l.id_lang = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['l.name', 'l.iso_code']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR l.id_lang = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3018,8 +3196,8 @@ class EntitySearchEngine
$sql->innerJoin('lang_shop', 'ls', 'ls.id_lang = l.id_lang AND ls.id_shop = ' . (int) $idShop);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(l.name LIKE \'%' . $escapedQuery . '%\' OR l.iso_code LIKE \'%' . $escapedQuery . '%\' OR l.id_lang = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['l.name', 'l.iso_code']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR l.id_lang = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3097,8 +3275,8 @@ class EntitySearchEngine
$sql->leftJoin('shop_group', 'sg', 'sg.id_shop_group = s.id_shop_group');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_shop = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_shop = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3150,8 +3328,8 @@ class EntitySearchEngine
$sql->from('shop', 's');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_shop = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_shop = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3227,8 +3405,8 @@ class EntitySearchEngine
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.id_profile = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['pl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_profile = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3274,8 +3452,8 @@ class EntitySearchEngine
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.id_profile = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['pl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_profile = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3348,8 +3526,8 @@ class EntitySearchEngine
$sql->where('os.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(osl.name LIKE \'%' . $escapedQuery . '%\' OR os.id_order_state = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['osl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR os.id_order_state = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3401,8 +3579,8 @@ class EntitySearchEngine
$sql->where('os.deleted = 0');
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(osl.name LIKE \'%' . $escapedQuery . '%\' OR os.id_order_state = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['osl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR os.id_order_state = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3475,8 +3653,8 @@ class EntitySearchEngine
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(tl.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tax = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['tl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tax = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3529,8 +3707,8 @@ class EntitySearchEngine
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(tl.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tax = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['tl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tax = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3610,8 +3788,8 @@ class EntitySearchEngine
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(al.name LIKE \'%' . $escapedQuery . '%\' OR agl.name LIKE \'%' . $escapedQuery . '%\' OR a.id_attribute = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['al.name', 'agl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR a.id_attribute = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3677,8 +3855,8 @@ class EntitySearchEngine
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(al.name LIKE \'%' . $escapedQuery . '%\' OR agl.name LIKE \'%' . $escapedQuery . '%\' OR a.id_attribute = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['al.name', 'agl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR a.id_attribute = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3762,8 +3940,8 @@ class EntitySearchEngine
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(fvl.value LIKE \'%' . $escapedQuery . '%\' OR fl.name LIKE \'%' . $escapedQuery . '%\' OR fv.id_feature_value = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['fvl.value', 'fl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR fv.id_feature_value = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3825,8 +4003,8 @@ class EntitySearchEngine
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(fvl.value LIKE \'%' . $escapedQuery . '%\' OR fl.name LIKE \'%' . $escapedQuery . '%\' OR fv.id_feature_value = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['fvl.value', 'fl.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR fv.id_feature_value = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3911,8 +4089,8 @@ class EntitySearchEngine
$sql->where('t.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(t.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tag = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['t.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tag = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -3960,8 +4138,8 @@ class EntitySearchEngine
$sql->where('t.id_lang = ' . (int) $idLang);
if (!empty($query)) {
$escapedQuery = $this->escapePattern($query);
$sql->where('(t.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tag = ' . (int) $query . ')');
$smartWhere = $this->buildSmartSearch($query, ['t.name']);
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tag = ' . (int) $query . ')');
}
if (!empty($filters['refine'])) {
@@ -4025,7 +4203,6 @@ class EntitySearchEngine
public function searchTargetMprMaterials($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
{
$db = Db::getInstance();
$escapedQuery = $this->escapePattern($query);
$sql = new DbQuery();
$sql->select('m.id_material, m.reference, m.unit, m.unit_cost, m.active,
@@ -4036,7 +4213,8 @@ class EntitySearchEngine
$sql->leftJoin('mprwarehouserevolution_material_stock', 'ms', 'ms.id_material = m.id_material');
if ($query !== '') {
$sql->where('(ml.name LIKE \'%' . $escapedQuery . '%\' OR m.reference LIKE \'%' . $escapedQuery . '%\' OR m.barcode LIKE \'%' . $escapedQuery . '%\')');
$smartWhere = $this->buildSmartSearch($query, ['ml.name', 'm.reference', 'm.barcode']);
if ($smartWhere) { $sql->where($smartWhere); }
}
$sql->groupBy('m.id_material');
@@ -4073,15 +4251,14 @@ class EntitySearchEngine
*/
public function countTargetMprMaterials($query, $idLang, $idShop, array $filters = [])
{
$escapedQuery = $this->escapePattern($query);
$sql = new DbQuery();
$sql->select('COUNT(DISTINCT m.id_material)');
$sql->from('mprwarehouserevolution_material', 'm');
$sql->leftJoin('mprwarehouserevolution_material_lang', 'ml', 'ml.id_material = m.id_material AND ml.id_lang = ' . (int) $idLang);
if ($query !== '') {
$sql->where('(ml.name LIKE \'%' . $escapedQuery . '%\' OR m.reference LIKE \'%' . $escapedQuery . '%\' OR m.barcode LIKE \'%' . $escapedQuery . '%\')');
$smartWhere = $this->buildSmartSearch($query, ['ml.name', 'm.reference', 'm.barcode']);
if ($smartWhere) { $sql->where($smartWhere); }
}
return (int) Db::getInstance()->getValue($sql);

View File

@@ -53,210 +53,6 @@ class EntitySelectorRenderer
$this->translator = $translator;
}
// ---------------------------------------------------------------
// Icon framework abstraction (Material Icons vs FontAwesome 4)
// ---------------------------------------------------------------
/**
* Material Icons → FontAwesome 4 class mapping
*/
private static $fa4Map = [
'account_tree' => 'icon-sitemap',
'add' => 'icon-plus',
'add_box' => 'icon-plus-square',
'arrow_downward' => 'icon-sort-desc',
'arrow_drop_down' => 'icon-caret-down',
'arrow_right' => 'icon-chevron-right',
'arrow_upward' => 'icon-sort-asc',
'block' => 'icon-ban',
'brush' => 'icon-paint-brush',
'business' => 'icon-building',
'check' => 'icon-check',
'check_box' => 'icon-check-square',
'check_box_outline_blank' => 'icon-square-o',
'check_circle' => 'icon-check-circle',
'close' => 'icon-times',
'delete' => 'icon-trash',
'description' => 'icon-file-text',
'error' => 'icon-exclamation-circle',
'event' => 'icon-calendar',
'event_busy' => 'icon-calendar-times-o',
'expand_less' => 'icon-chevron-up',
'expand_more' => 'icon-chevron-down',
'filter_list' => 'icon-filter',
'flag' => 'icon-flag',
'folder' => 'icon-folder',
'folder_open' => 'icon-folder-open',
'indeterminate_check_box' => 'icon-minus-square',
'info' => 'icon-info-circle',
'inventory_2' => 'icon-archive',
'label' => 'icon-tag',
'language' => 'icon-globe',
'lightbulb' => 'icon-lightbulb-o',
'list' => 'icon-list',
'list_alt' => 'icon-list-alt',
'local_shipping' => 'icon-truck',
'lock' => 'icon-lock',
'my_location' => 'icon-crosshairs',
'open_in_full' => 'icon-expand',
'payments' => 'icon-credit-card',
'progress_activity' => 'icon-circle-o-notch',
'schedule' => 'icon-clock-o',
'search' => 'icon-search',
'shopping_cart' => 'icon-shopping-cart',
'sort' => 'icon-sort',
'sort_by_alpha' => 'icon-sort-alpha-asc',
'star' => 'icon-star',
'sync' => 'icon-refresh',
'tune' => 'icon-sliders',
'visibility' => 'icon-eye',
'warning' => 'icon-warning',
'widgets' => 'icon-th-large',
];
/**
* FontAwesome 4 class → Material Icons reverse mapping.
* Built once from $fa4Map on first use.
* @var array|null
*/
private static $reverseFa4Map = null;
/**
* Extra FA4→Material mappings for icon names used in block configs
* that don't appear in the standard fa4Map (e.g. icon-cube, icon-folder-o).
*/
private static $extraReverseMappings = [
'icon-cube' => 'inventory',
'icon-folder-o' => 'folder',
'icon-file-text-o' => 'description',
'icon-briefcase' => 'work',
'icon-user' => 'person',
'icon-users' => 'group',
'icon-money' => 'payments',
'icon-tasks' => 'checklist',
'icon-calculator' => 'calculate',
'icon-asterisk' => 'star',
'icon-bar-chart' => 'bar_chart',
'icon-cogs' => 'settings',
'icon-cog' => 'settings',
'icon-tags' => 'label',
'icon-list-ul' => 'list',
'icon-th' => 'grid_view',
'icon-certificate' => 'verified',
'icon-power-off' => 'power_settings_new',
'icon-circle-o' => 'radio_button_unchecked',
];
/**
* Get the reverse FA4→Material mapping (built lazily from $fa4Map + extras).
*
* @return array
*/
private static function getReverseFa4Map()
{
if (self::$reverseFa4Map === null) {
self::$reverseFa4Map = array_flip(self::$fa4Map);
// Merge extras (extras take priority for icons not in the flipped map)
foreach (self::$extraReverseMappings as $fa4Class => $materialName) {
if (!isset(self::$reverseFa4Map[$fa4Class])) {
self::$reverseFa4Map[$fa4Class] = $materialName;
}
}
}
return self::$reverseFa4Map;
}
/**
* Normalize an icon name to the canonical format for the current mode.
* Handles both Material Icons names and FA4 class names as input.
*
* @param string $name Icon name (Material or FA4 format)
* @return array ['name' => string, 'extra' => string] normalized name + any extra classes
*/
protected function normalizeIconName($name)
{
$extra = '';
// If name starts with 'icon-', it's an FA4 class name
if (strpos($name, 'icon-') === 0) {
// Extract extra CSS classes (e.g. "icon-power-off text-success" → "icon-power-off" + "text-success")
$parts = explode(' ', $name, 2);
$fa4Class = $parts[0];
if (isset($parts[1])) {
$extra = $parts[1];
}
if ($this->getIconMode() === 'material') {
// Reverse map FA4→Material
$reverseMap = self::getReverseFa4Map();
$materialName = $reverseMap[$fa4Class] ?? null;
if ($materialName) {
return ['name' => $materialName, 'extra' => $extra];
}
// Last resort: strip 'icon-' prefix and convert hyphens to underscores
$fallback = str_replace('-', '_', substr($fa4Class, 5));
return ['name' => $fallback, 'extra' => $extra];
}
// Already FA4 and mode is FA4 — use as-is
return ['name' => $fa4Class, 'extra' => $extra, 'raw_fa4' => true];
}
// Material Icons name — use as-is for material mode, map for FA4 mode
return ['name' => $name, 'extra' => $extra];
}
/**
* Detect icon mode based on PrestaShop version.
* PS 8+ / 9+ → material, PS 1.6 / 1.7 → fa4.
*
* @return string 'material' or 'fa4'
*/
protected function getIconMode()
{
return version_compare(_PS_VERSION_, '8.0.0', '>=') ? 'material' : 'fa4';
}
/**
* Render an icon element that works on both legacy and modern PS.
* Accepts both Material Icons names and FA4 class names as input.
*
* @param string $name Icon name (Material or FA4 format, e.g. 'shopping_cart' or 'icon-cube')
* @param string $extraClass Additional CSS class(es)
* @return string HTML
*/
protected function renderIcon($name, $extraClass = '')
{
$normalized = $this->normalizeIconName($name);
$iconName = $normalized['name'];
// Merge extra classes from normalization (e.g. "text-success" from "icon-power-off text-success")
if (!empty($normalized['extra'])) {
$extraClass = $extraClass ? $extraClass . ' ' . $normalized['extra'] : $normalized['extra'];
}
if ($this->getIconMode() === 'material') {
$cls = 'material-icons es-icon';
if ($extraClass) {
$cls .= ' ' . $extraClass;
}
return '<i class="' . $cls . '">' . htmlspecialchars($iconName, ENT_QUOTES, 'UTF-8') . '</i>';
}
// FA4 mode
if (!empty($normalized['raw_fa4'])) {
// Input was already an FA4 class name — use directly
$cls = $iconName . ' es-icon';
} else {
// Input was a Material name — map to FA4
$mapped = self::$fa4Map[$iconName] ?? 'icon-circle';
$cls = $mapped . ' es-icon';
}
if ($extraClass) {
$cls .= ' ' . $extraClass;
}
return '<i class="' . $cls . '"></i>';
}
/**
* Set block definitions
*
@@ -303,6 +99,21 @@ class EntitySelectorRenderer
return htmlspecialchars((string)$string, ENT_QUOTES, 'UTF-8');
}
/**
* Build a help icon with tooltip or details popover.
* Single source of truth for info icons across the entity selector.
*
* @param string $content HTML content for the tooltip/popover
* @param string $type 'tooltip' for hover tooltip, 'details' for click popover
* @return string HTML string
*/
protected function buildHelpIcon($content, $type = 'tooltip')
{
$attr = ($type === 'details') ? 'data-details' : 'data-tooltip';
return '<span class="mpr-info-wrapper" ' . $attr . '="' . $this->escapeAttr($content) . '">'
. '<i class="material-icons">info_outline</i></span>';
}
/**
* Render target conditions HTML
*
@@ -325,6 +136,7 @@ class EntitySelectorRenderer
'show_cms' => true,
'show_cms_categories' => true,
'combination_mode' => 'products',
'product_selection_level' => 'product',
'mode' => 'multi',
'blocks' => [],
'customBlocks' => [],
@@ -343,6 +155,9 @@ class EntitySelectorRenderer
if (is_string($savedData)) {
$savedData = json_decode($savedData, true) ?: [];
}
if (!is_array($savedData)) {
$savedData = [];
}
// Determine which block is active
$enabledBlocks = [];
@@ -366,7 +181,7 @@ class EntitySelectorRenderer
'label' => $blockDef['label'] ?? $blockType,
'entity_label' => $blockDef['label'] ?? $blockType,
'entity_label_plural' => $blockDef['label'] ?? $blockType,
'icon' => $blockDef['icon'] ?? 'settings',
'icon' => $blockDef['icon'] ?? 'icon-cog',
'search_entity' => $blockType,
'selection_methods' => [],
], $blockDef);
@@ -414,10 +229,9 @@ class EntitySelectorRenderer
}
// Standalone layout (default)
$html = '<div class="condition-trait target-conditions-trait' . $collapsedClass . $singleModeClass . $requiredClass . $layoutClass . '"';
$html = '<div class="condition-trait entity-selector-trait' . $collapsedClass . $singleModeClass . $requiredClass . $layoutClass . '"';
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
if (!empty($config['required'])) {
$html .= ' data-required="1"';
$requiredMsg = !empty($config['required_message'])
@@ -431,8 +245,8 @@ class EntitySelectorRenderer
$html .= $this->renderHeader($config, $hasAnyData, $globalMode);
// Body
$bodyStyle = !empty($collapsedClass) ? ' style="display:none;"' : '';
$html .= '<div class="condition-trait-body"' . $bodyStyle . '>';
$bodyExpandedClass = empty($collapsedClass) ? ' es-expanded' : '';
$html .= '<div class="condition-trait-body' . $bodyExpandedClass . '">';
// Tabs
$html .= $this->renderTabs($enabledBlocks, $activeBlock, $savedData, $config);
@@ -447,8 +261,7 @@ class EntitySelectorRenderer
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
$html .= '</div>'; // End condition-trait-body
$html .= '</div>'; // End target-conditions-trait
$html .= '</div>'; // End entity-selector-trait
return $html;
}
@@ -483,10 +296,9 @@ class EntitySelectorRenderer
$html .= '<div class="col-lg-9">';
// Entity selector container (without traditional header)
$html .= '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . '"';
$html .= '<div class="condition-trait entity-selector-trait layout-form-group' . $singleModeClass . $requiredClass . '"';
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
if (!empty($config['required'])) {
$html .= ' data-required="1"';
$requiredMsg = !empty($config['required_message'])
@@ -497,7 +309,7 @@ class EntitySelectorRenderer
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
// Body (always visible in form-group layout)
$html .= '<div class="condition-trait-body">';
$html .= '<div class="condition-trait-body es-expanded">';
// Tabs row with expand button
$html .= '<div class="entity-selector-tabs-row">';
@@ -507,7 +319,7 @@ class EntitySelectorRenderer
if ($globalMode !== 'single') {
$html .= '<div class="entity-selector-actions">';
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
$html .= $this->renderIcon('open_in_full');
$html .= '<i class="icon-resize-vertical"></i>';
$html .= '</button>';
$html .= '</div>';
}
@@ -523,8 +335,7 @@ class EntitySelectorRenderer
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
$html .= '</div>'; // End condition-trait-body
$html .= '</div>'; // End target-conditions-trait
$html .= '</div>'; // End entity-selector-trait
// Subtitle as help text
if (!empty($config['subtitle'])) {
@@ -559,10 +370,9 @@ class EntitySelectorRenderer
$collapsedClass = $collapsed ? ' blocks-collapsed' : '';
// Entity selector container (without form-group wrapper, without header)
$html = '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . $collapsedClass . '"';
$html = '<div class="condition-trait entity-selector-trait layout-form-group' . $singleModeClass . $requiredClass . $collapsedClass . '"';
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
if (!empty($config['required'])) {
$html .= ' data-required="1"';
$requiredMsg = !empty($config['required_message'])
@@ -578,13 +388,13 @@ class EntitySelectorRenderer
// Actions: expand/collapse toggle (entire area is clickable)
$html .= '<div class="entity-selector-actions btn-toggle-blocks" title="' . $this->trans('Show/hide details') . '">';
$html .= $this->renderIcon($collapsed ? 'expand_more' : 'expand_less');
$html .= '<i class="' . ($collapsed ? 'icon-chevron-down' : 'icon-chevron-up') . '"></i>';
$html .= '</div>';
$html .= '</div>'; // End tabs-row
// Blocks content (visible by default when not collapsed)
$blocksStyle = $collapsed ? ' style="display:none;"' : '';
$html .= '<div class="entity-selector-blocks-content"' . $blocksStyle . '>';
$blocksExpandedClass = $collapsed ? '' : ' es-expanded';
$html .= '<div class="entity-selector-blocks-content' . $blocksExpandedClass . '">';
// Blocks
$html .= $this->renderBlocks($enabledBlocks, $activeBlock, $savedData, $config, $globalMode);
@@ -597,8 +407,7 @@ class EntitySelectorRenderer
// Hidden input (outside collapsed area)
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
$html .= '</div>'; // End target-conditions-trait
$html .= '</div>'; // End entity-selector-trait
return $html;
}
@@ -615,12 +424,12 @@ class EntitySelectorRenderer
{
$html = '<div class="condition-trait-header">';
$html .= '<div class="trait-header-left">';
$html .= $this->renderIcon('my_location', 'trait-icon');
$html .= '<i class="icon-crosshairs trait-icon"></i>';
$html .= '<div class="trait-title-group">';
$html .= '<span class="trait-title">' . $this->escapeAttr($config['title']) . '</span>';
$html .= '<span class="trait-subtitle">' . $this->escapeAttr($config['subtitle']) . '</span>';
$html .= '</div>';
$html .= '<span class="trait-total-count" style="display: none;" title="' . $this->trans('Total items targeted') . '">' . $this->renderIcon('visibility') . ' <span class="count-value"></span></span>';
$html .= '<span class="trait-total-count" style="display: none;" title="' . $this->trans('Total items targeted') . '"><i class="icon-eye"></i> <span class="count-value"></span></span>';
$html .= '</div>';
$html .= '<div class="trait-header-right">';
@@ -635,7 +444,7 @@ class EntitySelectorRenderer
if ($globalMode !== 'single') {
$html .= '<div class="trait-header-actions">';
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
$html .= $this->renderIcon('open_in_full');
$html .= '<i class="icon-expand"></i>';
$html .= '</button>';
$html .= '</div>';
}
@@ -677,17 +486,17 @@ class EntitySelectorRenderer
if ($emptyMeansAllToggle) {
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_on" value="1"';
$html .= ' class="target-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
$html .= ' class="es-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
$html .= '<label for="' . $switchName . '_on">' . $this->escapeAttr($emptyLabel) . '</label>';
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_off" value="0"';
$html .= ' class="target-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
$html .= ' class="es-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
$html .= '<label for="' . $switchName . '_off">' . $this->escapeAttr($selectedLabel) . '</label>';
} else {
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_on" value="0"';
$html .= ' class="target-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
$html .= ' class="es-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
$html .= '<label for="' . $switchName . '_on">' . $this->escapeAttr($selectedLabel) . '</label>';
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_off" value="1"';
$html .= ' class="target-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
$html .= ' class="es-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
$html .= '<label for="' . $switchName . '_off">' . $this->escapeAttr($emptyLabel) . '</label>';
}
@@ -709,7 +518,7 @@ class EntitySelectorRenderer
protected function renderTabs($enabledBlocks, $activeBlock, $savedData, $config)
{
$blockSettings = $config['blocks'] ?? [];
$html = '<div class="target-block-tabs">';
$html = '<div class="es-block-tabs">';
foreach ($enabledBlocks as $blockType => $blockDef) {
$isActive = ($blockType === $activeBlock);
@@ -718,12 +527,12 @@ class EntitySelectorRenderer
$hasDataClass = $hasData ? ' has-data' : '';
$blockMode = $blockSettings[$blockType]['mode'] ?? 'multi';
$html .= '<button type="button" class="target-block-tab' . $activeClass . $hasDataClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-block-mode="' . $this->escapeAttr($blockMode) . '">';
$html .= $this->renderIcon($blockDef['icon']);
$html .= '<button type="button" class="es-block-tab' . $activeClass . $hasDataClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-block-mode="' . $this->escapeAttr($blockMode) . '">';
$html .= '<i class="' . $this->escapeAttr($blockDef['icon']) . '"></i>';
$html .= '<span class="tab-label">' . $this->escapeAttr($blockDef['label']) . '</span>';
if ($hasData) {
// Show loading spinner that will be replaced with actual count
$html .= '<span class="tab-badge loading">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>';
}
$html .= '</button>';
}
@@ -746,7 +555,7 @@ class EntitySelectorRenderer
{
$blockSettings = $config['blocks'] ?? [];
$emptyMeansAll = $config['empty_means_all'] ?? true;
$html = '<div class="target-blocks-wrapper">';
$html = '<div class="es-blocks-wrapper">';
foreach ($enabledBlocks as $blockType => $blockDef) {
$isActive = ($blockType === $activeBlock);
@@ -766,18 +575,18 @@ class EntitySelectorRenderer
*/
protected function renderTipsBox()
{
$html = '<div class="target-tips-box">';
$html = '<div class="es-tips-box">';
$html .= '<div class="tips-header">';
$html .= $this->renderIcon('lightbulb');
$html .= '<i class="icon-lightbulb-o"></i>';
$html .= '<span>' . $this->trans('Pro Tips: Combine include & exclude for powerful targeting') . '</span>';
$html .= $this->renderIcon('expand_more', 'tips-toggle');
$html .= '<i class="icon-chevron-down tips-toggle"></i>';
$html .= '</div>';
$html .= '<div class="tips-content">';
$html .= '<div class="tips-grid">';
// Example 1
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon">' . $this->renderIcon('check_circle') . '</div>';
$html .= '<div class="tip-icon"><i class="icon-check-circle"></i></div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Target entire catalog with exceptions') . '</strong>';
$html .= '<p>' . $this->trans('Select "All products", then exclude specific categories like "Sale" or "Clearance" where you don\'t want the rule to apply.') . '</p>';
@@ -786,7 +595,7 @@ class EntitySelectorRenderer
// Example 2
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon">' . $this->renderIcon('filter_list') . '</div>';
$html .= '<div class="tip-icon"><i class="icon-filter"></i></div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Combine features for precise filtering') . '</strong>';
$html .= '<p>' . $this->trans('Target all "Cotton" products, then exclude those with "Black" color feature. Perfect for material-specific promotions.') . '</p>';
@@ -795,7 +604,7 @@ class EntitySelectorRenderer
// Example 3
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon">' . $this->renderIcon('account_tree') . '</div>';
$html .= '<div class="tip-icon"><i class="icon-sitemap"></i></div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Category-based targeting') . '</strong>';
$html .= '<p>' . $this->trans('Include entire "Men\'s Clothing" category, exclude "Accessories" subcategory. Hierarchy is respected automatically.') . '</p>';
@@ -804,7 +613,7 @@ class EntitySelectorRenderer
// Example 4
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon">' . $this->renderIcon('business') . '</div>';
$html .= '<div class="tip-icon"><i class="icon-building"></i></div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Brand exclusions') . '</strong>';
$html .= '<p>' . $this->trans('Target all products from "Nike" manufacturer, but exclude items already on sale (by price range or specific products).') . '</p>';
@@ -813,11 +622,11 @@ class EntitySelectorRenderer
$html .= '</div>'; // End tips-grid
$html .= '<div class="tips-footer">';
$html .= $this->renderIcon('info') . ' ';
$html .= '<i class="material-icons">info_outline</i> ';
$html .= $this->trans('Multiple groups work as OR logic. Items matching ANY group are included (unless explicitly excluded in that group).');
$html .= '</div>';
$html .= '</div>'; // End tips-content
$html .= '</div>'; // End target-tips-box
$html .= '</div>'; // End es-tips-box
return $html;
}
@@ -843,7 +652,7 @@ class EntitySelectorRenderer
$isCustomBlock = !empty($blockDef['custom']);
$customClass = $isCustomBlock ? ' custom-block' : '';
$html = '<div class="target-block' . $activeClass . $modeClass . $customClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-mode="' . $this->escapeAttr($mode) . '"' . $displayStyle . '>';
$html = '<div class="es-block' . $activeClass . $modeClass . $customClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-mode="' . $this->escapeAttr($mode) . '"' . $displayStyle . '>';
$html .= '<div class="block-body">';
// Custom blocks render their own HTML
@@ -913,16 +722,14 @@ class EntitySelectorRenderer
$html .= '<div class="block-footer">';
$html .= '<button type="button" class="btn-add-group">';
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add selection group');
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add selection group');
$html .= '</button>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($groupsTooltip) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
$html .= $this->buildHelpIcon($groupsTooltip, 'details');
$html .= '</div>';
}
$html .= '</div>'; // End block-body
$html .= '</div>'; // End target-block
$html .= '</div>'; // End es-block
return $html;
}
@@ -974,12 +781,10 @@ class EntitySelectorRenderer
$html = '<div class="result-modifiers-section">';
$html .= '<div class="result-modifiers-header">';
$html .= $this->renderIcon('tune') . ' ';
$html .= '<i class="icon-sliders"></i> ';
$html .= '<span class="result-modifiers-title">' . $this->trans('Result modifiers') . '</span>';
$html .= '<span class="result-modifiers-hint">' . $this->trans('(optional)') . '</span>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($modifiersTooltip) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
$html .= $this->buildHelpIcon($modifiersTooltip, 'details');
$html .= '</div>';
$html .= '<div class="result-modifiers-content">';
@@ -1117,17 +922,17 @@ class EntitySelectorRenderer
// Group header
if ($mode === 'single') {
$html .= '<div class="group-header group-header-single">';
$html .= '<span class="group-count-badge" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
$html .= '</div>';
} else {
$html .= '<div class="group-header">';
$html .= '<span class="group-collapse-toggle">' . $this->renderIcon('expand_less') . '</span>';
$html .= '<span class="group-collapse-toggle"><i class="icon-chevron-up"></i></span>';
$html .= '<span class="group-name-wrapper">';
$html .= '<input type="text" class="group-name-input" value="' . $groupName . '" placeholder="' . $defaultGroupName . '" title="' . $this->trans('Click to name this group') . '">';
$html .= '<span class="group-count-badge" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
$html .= '</span>';
$html .= '<button type="button" class="btn-remove-group" title="' . $this->trans('Remove group') . '">';
$html .= $this->renderIcon('delete');
$html .= '<i class="icon-trash"></i>';
$html .= '</button>';
$html .= '</div>';
}
@@ -1144,15 +949,13 @@ class EntitySelectorRenderer
$methodHelp = $this->getMethodHelpTooltip($includeMethod, $blockType);
$html .= '<span class="method-info-placeholder">';
if (!empty($methodHelp)) {
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
$html .= $this->buildHelpIcon($methodHelp, 'details');
}
$html .= '</span>';
$html .= '<select class="include-method-select">';
$html .= $this->renderMethodOptions($methods, $includeMethod, false);
$html .= '</select>';
$html .= '<span class="condition-match-count no-matches">' . $this->renderIcon('visibility') . ' <span class="preview-count">0</span></span>';
$html .= '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
$html .= '</div>';
// Value picker
@@ -1167,7 +970,7 @@ class EntitySelectorRenderer
if ($hasExcludes) {
$html .= '<div class="except-separator">';
$html .= '<span class="except-label">' . $this->renderIcon('block') . ' ' . $this->trans('EXCEPT') . '</span>';
$html .= '<span class="except-label"><i class="icon-ban"></i> ' . $this->trans('EXCEPT') . '</span>';
$html .= '</div>';
$html .= '<div class="exclude-rows-container">';
@@ -1177,11 +980,11 @@ class EntitySelectorRenderer
$html .= '</div>';
$html .= '<button type="button" class="btn-add-another-exclude">';
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add another exception');
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add another exception');
$html .= '</button>';
} else {
$html .= '<button type="button" class="btn-add-exclude">';
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add exceptions');
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add exceptions');
$html .= '</button>';
}
@@ -1239,15 +1042,15 @@ class EntitySelectorRenderer
$html .= '<option value="' . $this->escapeAttr($value) . '"' . $selected . '>' . $this->escapeAttr($label) . '</option>';
}
$html .= '</select>';
$sortDirIcon = ($sortDir === 'ASC') ? 'arrow_upward' : 'arrow_downward';
$sortDirIcon = ($sortDir === 'ASC') ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
$html .= '<button type="button" class="btn-sort-dir" data-dir="' . $this->escapeAttr($sortDir) . '" title="' . $this->trans('Sort direction') . '">';
$html .= $this->renderIcon($sortDirIcon);
$html .= '<i class="' . $sortDirIcon . '"></i>';
$html .= '</button>';
$html .= '</span>';
// Preview badge
$html .= '<span class="group-preview-badge clickable" title="' . $this->trans('Preview results') . '">';
$html .= $this->renderIcon('visibility') . ' <span class="preview-count"></span>';
$html .= '<i class="icon-eye"></i> <span class="preview-count"></span>';
$html .= '</span>';
$html .= '</div>';
@@ -1361,9 +1164,7 @@ class EntitySelectorRenderer
$methodHelp = $this->getMethodHelpTooltip($excludeMethod, $blockType);
$html .= '<span class="method-info-placeholder">';
if (!empty($methodHelp)) {
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
$html .= $this->buildHelpIcon($methodHelp, 'details');
}
$html .= '</span>';
@@ -1371,11 +1172,11 @@ class EntitySelectorRenderer
$html .= $this->renderMethodOptions($methods, $excludeMethod, true);
$html .= '</select>';
$html .= '<span class="condition-match-count no-matches">' . $this->renderIcon('visibility') . ' <span class="preview-count">0</span></span>';
$html .= '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
$html .= '</div>'; // End method-selector-wrapper
$html .= '<button type="button" class="btn-remove-exclude-row" title="' . $this->trans('Remove this exception') . '">';
$html .= $this->renderIcon('delete');
$html .= '<i class="icon-trash"></i>';
$html .= '</button>';
$html .= '</div>';
@@ -1418,12 +1219,15 @@ class EntitySelectorRenderer
switch ($valueType) {
case 'entity_search':
$noItemsPlaceholder = $this->trans('No items selected - use search below');
// Don't pre-wrap chips - JS will create the wrapper with toolbar when chips are added
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . $this->escapeAttr($noItemsPlaceholder) . '"></div>';
$html .= '<div class="chips-wrapper">';
$html .= '<div class="chips-toolbar" style="display:none;"></div>';
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . $this->escapeAttr($noItemsPlaceholder) . '"><span class="chips-empty-state">' . htmlspecialchars($noItemsPlaceholder) . '</span></div>';
$html .= '<div class="chips-load-more" style="display:none;"></div>';
$html .= '</div>';
$html .= '<div class="entity-search-box">';
$html .= $this->renderIcon('search', 'entity-search-icon');
$html .= '<i class="icon-search entity-search-icon"></i>';
$html .= '<input type="text" class="entity-search-input" placeholder="' . $this->trans('Search by name, reference, ID...') . '" autocomplete="off">';
$html .= '<span class="search-loading" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
$html .= '</div>';
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($values)) . '">';
break;
@@ -1435,7 +1239,7 @@ class EntitySelectorRenderer
$html .= '<button type="button" class="btn-toggle-case" title="' . $this->escapeAttr($this->trans('Case insensitive - click to toggle')) . '"><span class="case-icon">aa</span></button>';
$html .= '<input type="text" class="pattern-input" value="" placeholder="' . $this->escapeAttr($this->trans('e.g. *cotton*')) . '">';
$html .= '<span class="pattern-match-count"></span>';
$html .= '<button type="button" class="btn-add-pattern" title="' . $this->escapeAttr($this->trans('Add pattern (Enter)')) . '">' . $this->renderIcon('add') . '</button>';
$html .= '<button type="button" class="btn-add-pattern" title="' . $this->escapeAttr($this->trans('Add pattern (Enter)')) . '"><i class="icon-plus"></i></button>';
$html .= '</div>';
$html .= '</div>';
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($values)) . '">';
@@ -1464,7 +1268,7 @@ class EntitySelectorRenderer
$html .= '<input type="number" class="range-min-input" value="" placeholder="' . $this->trans('Min') . '" step="0.01">';
$html .= '<span class="range-separator">-</span>';
$html .= '<input type="number" class="range-max-input" value="" placeholder="' . $this->trans('Max') . '" step="0.01">';
$html .= '<button type="button" class="btn-add-range" title="' . $this->trans('Add range') . '">' . $this->renderIcon('add') . '</button>';
$html .= '<button type="button" class="btn-add-range" title="' . $this->trans('Add range') . '"><i class="icon-plus"></i></button>';
$html .= '</div>';
$html .= '</div>';
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($ranges)) . '">';
@@ -1481,7 +1285,7 @@ class EntitySelectorRenderer
$optIcon = is_array($optData) ? ($optData['icon'] ?? '') : '';
$html .= '<button type="button" class="tile-option' . $selectedClass . '" data-value="' . $this->escapeAttr($optKey) . '">';
if ($optIcon) {
$html .= $this->renderIcon($optIcon) . ' ';
$html .= '<i class="' . $this->escapeAttr($optIcon) . '"></i> ';
}
$html .= '<span class="tile-label">' . $this->escapeAttr($optLabel) . '</span>';
$html .= '</button>';
@@ -1752,6 +1556,7 @@ class EntitySelectorRenderer
],
'methodHelp' => $this->getAllMethodHelpContent(),
'combinationMode' => $config['combination_mode'] ?? 'products',
'productSelectionLevel' => $config['product_selection_level'] ?? 'product',
'emptyMeansAll' => $config['empty_means_all'] ?? true,
];
}
@@ -1787,23 +1592,23 @@ class EntitySelectorRenderer
switch ($sortBy) {
case 'name':
return $isAsc ? 'sort_by_alpha' : 'sort_by_alpha';
return $isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
case 'price':
case 'quantity':
case 'product_count':
return $isAsc ? 'sort' : 'sort';
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'date_add':
case 'newest_products':
return $isAsc ? 'sort' : 'sort';
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'sales':
case 'total_sales':
return $isAsc ? 'arrow_upward' : 'arrow_downward';
return $isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
case 'position':
return $isAsc ? 'sort' : 'sort';
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'random':
return 'shuffle';
return 'icon-random';
default:
return $isAsc ? 'arrow_upward' : 'arrow_downward';
return $isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
}
}
}

View File

@@ -124,7 +124,7 @@ trait ScheduleConditions
$assetPath = $this->getScheduleConditionsAssetPath();
// Load combined Tailwind CSS (includes target-conditions, modal, condition-traits, list-preview)
// Load combined Tailwind CSS (includes entity-selector, modal, condition-traits, list-preview)
$this->addCSS($assetPath . 'css/admin/tailwind-output.css');
$this->addJS($assetPath . 'js/admin/schedule-conditions.js');
@@ -254,7 +254,7 @@ trait ScheduleConditions
$html .= '</div>';
// Collapsible body
$html .= '<div class="condition-trait-body"' . ($scheduleEnabled ? '' : ' style="display:none;"') . '>';
$html .= '<div class="condition-trait-body' . ($scheduleEnabled ? ' es-expanded' : '') . '">';
// Datetime Range Section
if ($config['show_datetime_range']) {
@@ -761,7 +761,7 @@ trait ScheduleConditions
/**
* Render TargetConditions block for holiday countries
* Creates a standalone target-conditions widget for country selection
* Creates a standalone entity-selector widget for country selection
*
* @param string $prefix Form field prefix
* @param array $savedData Saved target conditions data
@@ -800,16 +800,16 @@ trait ScheduleConditions
$jsConfigJson = htmlspecialchars(json_encode($jsConfig), ENT_QUOTES, 'UTF-8');
$html = '<div class="target-conditions-trait holiday-countries-target layout-form-group"';
$html = '<div class="entity-selector-trait holiday-countries-target layout-form-group"';
$html .= ' data-entity-selector-id="holiday-countries-target"';
$html .= ' data-mode="multi"';
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
// Hidden input for serialized data
$html .= '<input type="hidden" name="' . $prefix . 'holiday_countries_data" class="target-conditions-data" value="' . htmlspecialchars(json_encode($savedData)) . '">';
$html .= '<input type="hidden" name="' . $prefix . 'holiday_countries_data" class="es-conditions-data" value="' . htmlspecialchars(json_encode($savedData)) . '">';
// Target blocks wrapper (single block for countries)
$html .= '<div class="target-blocks-wrapper">';
$html .= '<div class="es-blocks-wrapper">';
$html .= $this->renderHolidayCountriesBlock($blockType, $methods, $groups);
$html .= '</div>';
@@ -826,7 +826,7 @@ trait ScheduleConditions
$hasGroups = !empty($groups);
$emptyStateText = $this->transScheduleConditions('All countries included');
$html = '<div class="target-block active" data-block-type="' . htmlspecialchars($blockType) . '" data-mode="multi">';
$html = '<div class="es-block active" data-block-type="' . htmlspecialchars($blockType) . '" data-mode="multi">';
$html .= '<div class="block-body">';
$html .= '<div class="groups-container">';
@@ -861,7 +861,7 @@ trait ScheduleConditions
$html .= '</div>';
$html .= '</div>'; // End block-body
$html .= '</div>'; // End target-block
$html .= '</div>'; // End es-block
return $html;
}
@@ -1033,7 +1033,11 @@ trait ScheduleConditions
switch ($valueType) {
case 'entity_search':
$noItemsPlaceholder = $this->transScheduleConditions('No items selected - use search below');
$html .= '<div class="chips-wrapper">';
$html .= '<div class="chips-toolbar"></div>';
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . htmlspecialchars($noItemsPlaceholder) . '"></div>';
$html .= '<div class="chips-load-more"></div>';
$html .= '</div>';
$html .= '<div class="entity-search-box">';
$html .= '<i class="icon-search entity-search-icon"></i>';
$html .= '<input type="text" class="entity-search-input" placeholder="' . $this->transScheduleConditions('Search by name, reference, ID...') . '" autocomplete="off">';