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

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);