feat: icon abstraction layer for PS 1.6-9.x compatibility

Add esIcon() JS helper and renderIcon() PHP helper that auto-detect
the icon framework (Material Icons on PS 8+/9+, FontAwesome 4 on PS
1.6/1.7) and render appropriate HTML. Includes bidirectional mapping
between FA4 class names and Material Icons names.

- Replace all hardcoded <i class="material-icons"> with esIcon()/renderIcon()
- Add FA4_MAP (Material→FA4) and reverse FA4→Material mapping in both JS and PHP
- Add detectIconMode() with PHP data-attribute hint and font-family probe fallback
- Fix 4 self/this scope bugs in esIcon calls across _methods.js, _tree.js, _preview.js
- Add esIconUpdate() for dynamically updating existing icon elements
- Add normalizeIconName() to handle both FA4 and Material input formats

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 14:47:50 +01:00
parent 55e3135903
commit 1945da88b2
82 changed files with 56654 additions and 5364 deletions

View File

@@ -81,7 +81,7 @@
// Country: show flag
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\';"><i class="icon-flag flag-fallback" style="display:none;"></i></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\';">' + this.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '</span>';
} else if (data && data.image) {
html += '<span class="chip-icon"><img src="' + this.escapeAttr(data.image) + '" alt=""></span>';
}
@@ -90,10 +90,10 @@
// Country: add holiday preview button
if (isCountry) {
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays">' + this.esIcon('visibility') + '</button>';
}
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '<button type="button" class="chip-remove" title="Remove">' + this.esIcon('close') + '</button>';
html += '</span>';
$chips.append(html);
@@ -183,7 +183,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;">' +
'<i class="icon-chevron-up"></i> ' + collapseText +
this.esIcon('expand_less') + ' ' + collapseText +
'</button>'
).show();
} else {
@@ -211,7 +211,7 @@
'</select>' +
'<span class="chips-count"></span>' +
'<button type="button" class="btn-chips-clear" title="' + (trans.clear_all || 'Clear all') + '">' +
'<i class="icon-trash"></i> <span class="clear-text">' + (trans.clear || 'Clear') + '</span>' +
this.esIcon('delete') + ' <span class="clear-text">' + (trans.clear || 'Clear') + '</span>' +
'</button>' +
'</div>' +
'<div class="chips-load-more" style="display:none;"></div>' +
@@ -422,20 +422,16 @@
console.log('[EntitySelector] Making bulk AJAX request for entities:', JSON.stringify(bulkRequest));
// Single bulk AJAX call for all entity types
var bulkAjaxData = {
ajax: 1,
action: 'getTargetEntitiesByIdsBulk',
trait: 'EntitySelector',
entities: JSON.stringify(bulkRequest)
};
if (self.config.productSelectionLevel && self.config.productSelectionLevel !== 'product') {
bulkAjaxData.product_selection_level = self.config.productSelectionLevel;
}
$.ajax({
url: self.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: bulkAjaxData,
data: {
ajax: 1,
action: 'getTargetEntitiesByIdsBulk',
trait: 'EntitySelector',
entities: JSON.stringify(bulkRequest)
},
success: function(response) {
console.log('[EntitySelector] AJAX response:', response);
if (!response.success || !response.entities) {
@@ -481,7 +477,7 @@
// Country: show flag
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\';"><i class="icon-flag flag-fallback" style="display:none;"></i></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\';">' + self.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '</span>';
} else if (entity.image) {
html += '<span class="chip-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
}
@@ -490,10 +486,10 @@
// Country: add holiday preview button
if (isCountry) {
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays">' + self.esIcon('visibility') + '</button>';
}
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '<button type="button" class="chip-remove" title="Remove">' + self.esIcon('close') + '</button>';
html += '</span>';
$loadingChip.replaceWith(html);
@@ -587,7 +583,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: '<i class="icon-times"></i>'
html: self.esIcon('close')
}));
$chipsContainer.append($chip);
@@ -640,7 +636,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"><i class="' + entityIcon + ' icon-spin-pulse"></i></span>';
html += '<span class="chip-icon">' + self.esIcon(entityIcon, 'es-spin-pulse') + '</span>';
html += '<span class="chip-name">Loading...</span>';
html += '</span>';
$chips.append(html);
@@ -701,7 +697,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: '<i class="icon-times"></i>'
html: self.esIcon('close')
}));
$chipsContainer.append($chip);
@@ -754,22 +750,18 @@
// Handle entity_search type - load via AJAX
var searchEntity = $picker.attr('data-search-entity') || blockType;
var pickerAjaxData = {
ajax: 1,
action: 'getTargetEntitiesByIds',
trait: 'EntitySelector',
entity_type: searchEntity,
ids: JSON.stringify(values)
};
if (this.config.productSelectionLevel && this.config.productSelectionLevel !== 'product') {
pickerAjaxData.product_selection_level = this.config.productSelectionLevel;
}
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: pickerAjaxData,
data: {
ajax: 1,
action: 'getTargetEntitiesByIds',
trait: 'EntitySelector',
entity_type: searchEntity,
ids: JSON.stringify(values)
},
success: function(response) {
if (response.success && response.entities) {
// Track which IDs were actually found (entities may have been deleted)
@@ -818,7 +810,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') + '"><i class="icon-trash"></i></button>';
html += '<button type="button" class="btn-remove-pattern" title="' + this.escapeAttr(trans.remove_pattern || 'Remove pattern') + '">' + this.esIcon('delete') + '</button>';
html += '</div>';
$chipsContainer.append(html);
},
@@ -857,7 +849,7 @@
var entityType = $block.data('blockType') || 'products';
// Show loading - keep eye icon, update count value
$countValue.html('<i class="icon-spinner icon-spin"></i>');
$countValue.html(this.esIcon('progress_activity', 'es-spin'));
$matchCount.show();
// Store pattern for click handler
@@ -956,7 +948,7 @@
var blockType = $block.data('blockType') || 'products';
// Show loading
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Store condition data on badge for popover
@@ -1022,7 +1014,7 @@
var entityType = $block.data('blockType') || 'products';
// Show loading state
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
$.ajax({
@@ -1164,7 +1156,7 @@
var blockType = $block.data('blockType') || 'products';
// Show loading
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Store condition data on badge for popover
@@ -1252,7 +1244,7 @@
}
// Show loading
$badge.html('<i class="icon-spinner icon-spin"></i>').show();
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
$.ajax({
url: this.config.ajaxUrl,
@@ -1271,7 +1263,7 @@
var excludeCount = response.exclude_count || 0;
// Update badge with eye icon and count
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
if (excludeCount > 0) {
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
}

View File

@@ -127,16 +127,11 @@
// Add fullwidth class to parent form-group (skip for form-group layout)
var hasLayoutFormGroup = this.$wrapper.hasClass('layout-form-group');
var $entitySelectorFormGroup = this.$wrapper.closest('.entity-selector-form-group');
console.log('[EntitySelector] hasLayoutFormGroup:', hasLayoutFormGroup);
console.log('[EntitySelector] closest .entity-selector-form-group:', $entitySelectorFormGroup.length);
if (!hasLayoutFormGroup && !$entitySelectorFormGroup.length) {
console.log('[EntitySelector] Adding condition-trait-fullwidth to form-group');
var $formGroup = this.$wrapper.closest('.form-group');
$formGroup.addClass('condition-trait-fullwidth');
$formGroup.find('.col-lg-offset-3').removeClass('col-lg-offset-3');
} else {
console.log('[EntitySelector] SKIPPING fullwidth - form-group layout detected');
}
this.createDropdown();

View File

@@ -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 += '<i class="icon-check-square-o"></i> ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += this.esIcon('check_box') + ' ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += '</button>';
html += '<button type="button" class="btn-clear-selection" title="' + (trans.clear_selection || 'Clear selection') + '">';
html += '<i class="icon-square-o"></i> ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += this.esIcon('check_box_outline_blank') + ' ' + (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 += '<i class="icon-sort-alpha-asc"></i>';
html += this.esIcon('sort_by_alpha');
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)') + '"><i class="icon-ban"></i></button>';
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '">' + this.esIcon('block') + '</button>';
html += '<input type="text" class="refine-input" placeholder="' + (trans.refine_short || 'Refine...') + '">';
html += '<button type="button" class="btn-clear-refine" style="display:none;"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-refine" style="display:none;">' + this.esIcon('close') + '</button>';
html += '</div>';
// Filter toggle button
html += '<button type="button" class="btn-toggle-filters" title="' + (trans.toggle_filters || 'Filters') + '">';
html += '<i class="icon-filter"></i>';
html += this.esIcon('filter_list');
html += '</button>';
// History button
html += '<button type="button" class="btn-show-history" title="' + (trans.recent_searches || 'Recent searches') + '">';
html += '<i class="icon-clock-o"></i>';
html += this.esIcon('schedule');
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 += '<i class="icon-times"></i>';
html += this.esIcon('close');
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"><i class="icon-tags"></i> ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<span class="filter-row-label">' + this.esIcon('label') + ' ' + (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"><i class="icon-list-ul"></i> ' + (trans.features || 'Features') + ':</span>';
html += '<span class="filter-row-label">' + this.esIcon('list') + ' ' + (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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (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"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (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"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (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"><i class="icon-sitemap"></i> ' + (trans.depth || 'Depth') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('account_tree') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (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"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (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"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (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"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (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"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (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"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (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"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (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"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (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"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (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"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (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"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (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"><i class="icon-tags"></i> ' + (trans.attribute_group || 'Group') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('label') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (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"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (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"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (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"><i class="icon-list-ul"></i> ' + (trans.feature_group || 'Group') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('list') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><i class="icon-globe"></i> ' + (trans.zone || 'Zone') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('language') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</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"><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 += '<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 += '</div>';
html += '</div>';

View File

@@ -107,9 +107,9 @@
!$(e.target).closest('.group-count-badge').length &&
!$(e.target).closest('.group-modifiers').length &&
!$(e.target).closest('.group-preview-badge').length &&
!$(e.target).closest('.toggle-count.clickable').length &&
!$(e.target).closest('.trait-total-count').length &&
!$(e.target).closest('.chip-preview-holidays').length &&
!$(e.target).closest('.chip-preview-btn').length) {
!$(e.target).closest('.chip-preview-holidays').length) {
self.hidePreviewPopover();
// Also close holiday popover
$('.holiday-preview-popover').remove();
@@ -139,7 +139,7 @@
this.$wrapper.on('click', '.btn-toggle-blocks', function(e) {
e.preventDefault();
var $blocksContent = self.$wrapper.find('.entity-selector-blocks-content');
var $icon = $(this).find('.material-icons');
var $icon = $(this).find('.es-icon');
$blocksContent.stop(true, true);
if ($blocksContent.is(':visible')) {
$blocksContent.slideUp(200);
@@ -152,29 +152,9 @@
}
});
// Filter chip/group preview eye button (unified handler)
this.$wrapper.on('click', '.chip-preview-btn', function(e) {
e.preventDefault();
e.stopPropagation();
var $btn = $(this);
if ($btn.hasClass('popover-open')) {
self.hidePreviewPopover();
} else {
var valueId = $btn.data('id');
var valueType = $btn.data('type');
var valueName = $btn.data('name');
var groupId = $btn.data('groupId');
if (valueId) {
// Value-level preview (specific attribute/feature value)
self.showFilterValuePreviewPopover($btn, valueId, valueType, valueName, groupId);
} else if (groupId) {
// Group-level preview (entire attribute/feature group)
self.showFilterGroupPreviewPopover($btn, groupId, valueType, valueName);
}
}
// Custom block input changes — update tab badge when value changes
this.$wrapper.on('input change', '.custom-block-content input, .custom-block-content textarea, .custom-block-content select', function() {
self.updateTabBadges();
});
// Group-level collapse toggle (click on group header or toggle icon)
@@ -190,23 +170,27 @@
});
// Toggle all groups (single button that switches between expand/collapse)
this.$wrapper.on('click', '.trait-header-actions .btn-toggle-groups', function(e) {
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) {
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').removeClass('icon-expand').addClass('icon-compress');
$btn.find('i').text('close_fullscreen');
console.log('[ES-DEBUG] Expanded all groups');
} 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').removeClass('icon-compress').addClass('icon-expand');
$btn.find('i').text('open_in_full');
console.log('[ES-DEBUG] Collapsed all groups');
}
});
@@ -526,8 +510,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"><i class="icon-check"></i></button>');
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel"><i class="icon-times"></i></button>');
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 $editActions = $('<span class="pattern-edit-actions"></span>').append($saveBtn, $cancelBtn);
$tag.addClass('editing').find('.pattern-tag-text').hide();
@@ -576,50 +560,6 @@
$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');
@@ -715,7 +655,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: '<i class="icon-times"></i>'
html: self.esIcon('close')
}));
$chipsContainer.append($chip);
@@ -871,11 +811,7 @@
$btn.attr('data-dir', newDir);
var $icon = $btn.find('i');
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');
}
$icon.replaceWith(this.esIcon('sort'));
self.serializeAllBlocks();
self.refreshGroupPreviewIfOpen($group);
@@ -1056,7 +992,7 @@
if (isSelected) {
// Remove from pending selections
self.pendingSelections = self.pendingSelections.filter(function(s) {
return String(s.id) !== String(id);
return parseInt(s.id, 10) !== parseInt(id, 10);
});
self.removeSelection($picker, id);
$item.toggleClass('selected');
@@ -1070,9 +1006,10 @@
return;
}
var currentSelection = self.getCurrentSingleSelection();
var activeBlockType = self.activeGroup.blockType;
var currentSelection = self.getCurrentSingleSelection(activeBlockType);
if (currentSelection) {
var newEntityType = self.activeGroup.blockType;
var newEntityType = activeBlockType;
self.showReplaceConfirmation(currentSelection, { name: name, entityType: newEntityType }, function() {
// Add to pending selections
self.pendingSelections.push({ id: id, name: name, data: $item.data() });
@@ -1083,7 +1020,7 @@
} else {
// Add to pending selections
var exists = self.pendingSelections.some(function(s) {
return String(s.id) === String(id);
return parseInt(s.id, 10) === parseInt(id, 10);
});
if (!exists) {
self.pendingSelections.push({ id: id, name: name, data: $item.data() });
@@ -1108,7 +1045,7 @@
// Also remove from pending selections if dropdown is open
if (self.pendingSelections) {
self.pendingSelections = self.pendingSelections.filter(function(s) {
return String(s.id) !== String(id);
return parseInt(s.id, 10) !== parseInt(id, 10);
});
}
@@ -1235,7 +1172,7 @@
// Add to pending selections for Save button
var exists = self.pendingSelections.some(function(s) {
return String(s.id) === String(id);
return parseInt(s.id, 10) === parseInt(id, 10);
});
if (!exists) {
self.pendingSelections.push({
@@ -1369,7 +1306,7 @@
var currentDir = $btn.data('dir');
var newDir = currentDir === 'ASC' ? 'DESC' : 'ASC';
$btn.data('dir', newDir);
$btn.find('i').attr('class', newDir === 'ASC' ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc');
$btn.find('i').replaceWith(this.esIcon('sort_by_alpha'));
self.currentSort.dir = newDir;
self.refreshSearch();
});
@@ -1383,8 +1320,7 @@
$item.toggleClass('collapsed');
var isCollapsed = $item.hasClass('collapsed');
$(this).find('i').toggleClass('icon-caret-down', !isCollapsed)
.toggleClass('icon-caret-right', isCollapsed);
$(this).find('i').text(isCollapsed ? 'arrow_right' : 'arrow_drop_down');
var descendants = self.findTreeDescendants($item, $allItems);
for (var i = 0; i < descendants.length; i++) {
@@ -1515,7 +1451,7 @@
$child.removeClass('selected');
}
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.attr('title', trans.select_with_children || 'Select with all children');
} else {
var section = self.activeGroup.section;
@@ -1553,7 +1489,7 @@
self.showValidationError(skipMsg);
}
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
}
@@ -1574,7 +1510,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').removeClass('icon-caret-right').addClass('icon-caret-down');
self.$dropdown.find('.tree-toggle i').replaceWith(this.esIcon('arrow_drop_down'));
});
// Tree view: Collapse all
@@ -1594,7 +1530,7 @@
if (level === minLevel) {
if (hasChildren) {
$item.addClass('collapsed');
$item.find('.tree-toggle i').removeClass('icon-caret-down').addClass('icon-caret-right');
$item.find('.tree-toggle i').replaceWith(self.esIcon('arrow_right'));
}
$item.show();
} else {
@@ -1850,6 +1786,10 @@
// Toggle filter group - show values
this.$dropdown.on('click', '.filter-group-toggle', function(e) {
// Ignore clicks on the preview badge
if ($(e.target).closest('.toggle-count.clickable').length) {
return;
}
e.preventDefault();
var $btn = $(this);
var groupId = $btn.data('group-id');
@@ -1866,26 +1806,20 @@
}
});
// Filter preview eye button (dropdown-level, since dropdown is appended to body)
this.$dropdown.on('click', '.chip-preview-btn', function(e) {
e.preventDefault();
// Filter group toggle count badge click for preview popover
this.$dropdown.on('click', '.filter-group-toggle .toggle-count.clickable', function(e) {
e.stopPropagation();
e.preventDefault();
var $btn = $(this);
var $badge = $(this);
var groupId = $badge.data('groupId');
var groupType = $badge.data('type');
var groupName = $badge.data('groupName');
if ($btn.hasClass('popover-open')) {
if ($badge.hasClass('popover-open')) {
self.hidePreviewPopover();
} else {
var valueId = $btn.data('id');
var valueType = $btn.data('type');
var valueName = $btn.data('name');
var groupId = $btn.data('groupId');
if (valueId) {
self.showFilterValuePreviewPopover($btn, valueId, valueType, valueName, groupId);
} else if (groupId) {
self.showFilterGroupPreviewPopover($btn, groupId, valueType, valueName);
}
self.showFilterGroupPreviewPopover($badge, groupId, groupType, groupName);
}
});
@@ -1994,104 +1928,6 @@
}
});
// 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');
$('.mpr-tooltip-fixed.pinned').remove();
return;
}
// Close any other pinned tooltips
$('.mpr-info-wrapper.pinned').removeClass('pinned').find('.material-icons').text('info');
$('.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');
$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

@@ -228,7 +228,6 @@
if (!this.$dropdown || !this.filterableData) return;
var self = this;
var previewLabel = self.config.trans && self.config.trans.preview || 'Preview';
// Render attribute group toggle buttons
var $attrContainer = this.$dropdown.find('.filter-attributes-container');
@@ -236,17 +235,12 @@
if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
this.filterableData.attributes.forEach(function(group) {
var html = '<span class="filter-chip-wrapper">';
html += '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
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">(' + group.count + ')</span>';
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 += '</button>';
html += '<button type="button" class="chip-preview-btn" data-group-id="' + group.id + '" data-type="attribute" data-name="' + self.escapeAttr(group.name) + '" title="' + previewLabel + '">';
html += '<i class="icon-eye"></i>';
html += '</button>';
html += '</span>';
$attrContainer.append(html);
});
this.$dropdown.find('.filter-row-attributes').show();
@@ -258,17 +252,12 @@
if (this.filterableData.features && this.filterableData.features.length > 0) {
this.filterableData.features.forEach(function(group) {
var html = '<span class="filter-chip-wrapper">';
html += '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
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">(' + group.count + ')</span>';
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 += '</button>';
html += '<button type="button" class="chip-preview-btn" data-group-id="' + group.id + '" data-type="feature" data-name="' + self.escapeAttr(group.name) + '" title="' + previewLabel + '">';
html += '<i class="icon-eye"></i>';
html += '</button>';
html += '</span>';
$featContainer.append(html);
});
this.$dropdown.find('.filter-row-features').show();
@@ -306,7 +295,6 @@
var colorStyle = val.color ? ' style="--chip-color: ' + val.color + '"' : '';
var colorClass = val.color ? ' has-color' : '';
html += '<span class="filter-chip-wrapper">';
html += '<button type="button" class="filter-chip ' + chipClass + activeClass + colorClass + '" data-id="' + val.id + '" data-group-id="' + groupId + '"' + colorStyle + '>';
if (val.color) {
html += '<span class="chip-color-dot"></span>';
@@ -316,17 +304,13 @@
html += '<span class="chip-count">(' + val.count + ')</span>';
}
html += '</button>';
html += '<button type="button" class="chip-preview-btn" data-id="' + val.id + '" data-group-id="' + groupId + '" data-type="' + type + '" data-name="' + self.escapeAttr(val.name) + '" title="' + (self.config.trans && self.config.trans.preview || 'Preview') + '">';
html += '<i class="icon-eye"></i>';
html += '</button>';
html += '</span>';
});
$valuesContainer.html(html);
// 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"><i class="icon-times"></i></button>');
$filterRowValues.append('<button type="button" class="btn-close-values">' + this.esIcon('close') + '</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"><i class="icon-chevron-up"></i></span>';
html += '<span class="group-collapse-toggle">' + this.esIcon('expand_less') + '</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;"><i class="icon-spinner icon-spin"></i></span>';
html += '<span class="group-count-badge" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '</span>';
html += '<button type="button" class="btn-remove-group" title="' + (trans.remove_group || 'Remove group') + '">';
html += '<i class="icon-trash"></i>';
html += this.esIcon('delete');
html += '</button>';
html += '</div>';
@@ -65,16 +65,16 @@
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"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
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 += '</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="entity-search-box">';
html += '<i class="icon-search entity-search-icon"></i>';
html += this.esIcon('search', 'entity-search-icon');
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;"><i class="icon-spinner icon-spin"></i></span>';
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '</div>';
html += '<input type="hidden" class="include-values-data" value="[]">';
html += '</div>';
@@ -84,7 +84,7 @@
// Excludes section (collapsed by default)
html += '<div class="group-excludes">';
html += '<button type="button" class="btn-add-exclude">';
html += '<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions');
html += this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions');
html += '</button>';
html += '</div>';
@@ -92,11 +92,11 @@
html += '<div class="group-modifiers">';
html += '<span class="modifier-inline modifier-limit">';
html += '<span class="modifier-label">' + (trans.limit || 'Limit') + '</span>';
html += '<input type="number" class="group-modifier-limit" placeholder="" min="1" step="1" title="' + (trans.limit_tooltip || 'Max items to return (empty = all)') + '">';
html += '<input type="number" class="group-modifier-limit mpr-input-compact" placeholder="" min="1" step="1" title="' + (trans.limit_tooltip || 'Max items to return (empty = all)') + '">';
html += '</span>';
html += '<span class="modifier-inline modifier-sort">';
html += '<span class="modifier-label">' + (trans.sort || 'Sort') + '</span>';
html += '<select class="group-modifier-sort">';
html += '<select class="group-modifier-sort mpr-input-compact">';
html += '<option value="sales" selected>' + (trans.sort_bestsellers || 'Best sellers') + '</option>';
html += '<option value="date_add">' + (trans.sort_newest || 'Newest') + '</option>';
html += '<option value="price">' + (trans.sort_price || 'Price') + '</option>';
@@ -106,11 +106,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 += '<i class="icon-sort-amount-desc"></i>';
html += this.esIcon('sort');
html += '</button>';
html += '</span>';
html += '<span class="group-preview-badge clickable" title="' + (trans.preview_results || 'Preview results') + '">';
html += '<i class="icon-eye"></i> <span class="preview-count"></span>';
html += this.esIcon('visibility') + ' <span class="preview-count"></span>';
html += '</span>';
html += '</div>';
@@ -213,12 +213,32 @@
if (groupCount > 0) {
// Show loading state first
if ($badge.length) {
$badge.addClass('loading').html('<i class="icon-spinner icon-spin"></i>');
$badge.addClass('loading').html(self.esIcon('progress_activity', 'es-spin'));
} else {
$tab.append('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
$tab.append('<span class="tab-badge loading">' + self.esIcon('progress_activity', 'es-spin') + '</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() !== '') {
hasCustomValue = true;
return false;
}
});
if (hasCustomValue) {
if ($badge.length) {
$badge.removeClass('loading').html(self.esIcon('check'));
} else {
$tab.append('<span class="tab-badge">' + self.esIcon('check') + '</span>');
}
$tab.addClass('has-data');
} else {
$badge.remove();
$tab.removeClass('has-data');
}
} else {
$badge.remove();
$tab.removeClass('has-data');
@@ -312,7 +332,7 @@
var $badge = $tab.find('.tab-badge');
if ($badge.length) {
$badge.removeClass('loading').html('<i class="icon-eye"></i> ' + count);
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + count);
// Store preview data for later popover use
$tab.data('previewData', { count: count, success: true });
}
@@ -377,10 +397,10 @@
// Show loading state
var $badge = $tab.find('.tab-badge');
if (!$badge.length) {
$badge = $('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
$badge = $('<span class="tab-badge loading">' + this.esIcon('progress_activity', 'es-spin') + '</span>');
$tab.append($badge);
} else {
$badge.addClass('loading').html('<i class="icon-spinner icon-spin"></i>');
$badge.addClass('loading').html(this.esIcon('progress_activity', 'es-spin'));
}
$tab.addClass('has-data');
@@ -401,7 +421,7 @@
success: function(response) {
if (response.success) {
var $badge = $tab.find('.tab-badge');
$badge.removeClass('loading').html('<i class="icon-eye"></i> ' + response.count);
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + response.count);
// Store preview data for popover
$tab.data('previewData', response);
@@ -979,7 +999,7 @@
}
// Show loading spinner
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Store condition data on badge for popover
@@ -1063,7 +1083,7 @@
// 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('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches country-holidays').show();
// First fetch all active country IDs, then get holidays
@@ -1170,7 +1190,7 @@
var isCountrySelection = (searchEntity === 'countries' && valueType === 'entity_search');
console.log('[updateConditionCount] isCountrySelection:', isCountrySelection, 'values:', values);
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches country-holidays').show();
// For countries, fetch holiday count
@@ -1283,7 +1303,7 @@
}
// Show loading
$badge.html('<i class="icon-spinner icon-spin"></i>').show();
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
$.ajax({
url: this.config.ajaxUrl,
@@ -1302,7 +1322,7 @@
var excludeCount = response.exclude_count || 0;
// Update badge with eye icon and count
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
if (excludeCount > 0) {
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
}
@@ -1343,7 +1363,7 @@
// Build the full excludes structure with first row
var html = '<div class="except-separator">';
html += '<span class="except-label"><i class="icon-ban"></i> ' + (trans.except || 'EXCEPT') + '</span>';
html += '<span class="except-label">' + this.esIcon('block') + ' ' + (trans.except || 'EXCEPT') + '</span>';
html += '</div>';
html += '<div class="exclude-rows-container">';
@@ -1351,7 +1371,7 @@
html += '</div>';
html += '<button type="button" class="btn-add-another-exclude">';
html += '<i class="icon-plus"></i> ' + (trans.add_another_exception || 'Add another exception');
html += this.esIcon('add') + ' ' + (trans.add_another_exception || 'Add another exception');
html += '</button>';
$excludesDiv.addClass('has-excludes').html(html);
@@ -1422,11 +1442,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"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
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 += '</div>';
html += '<button type="button" class="btn-remove-exclude-row" title="' + (trans.remove_this_exception || 'Remove this exception') + '">';
html += '<i class="icon-trash"></i>';
html += this.esIcon('delete');
html += '</button>';
html += '</div>';
@@ -1452,7 +1472,7 @@
var $excludesDiv = $group.find('.group-excludes');
$excludesDiv.removeClass('has-excludes').html(
'<button type="button" class="btn-add-exclude">' +
'<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions') +
this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions') +
'</button>'
);
// Unlock the method selector since no excludes exist
@@ -1558,9 +1578,9 @@
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="entity-search-box">';
html += '<i class="icon-search entity-search-icon"></i>';
html += this.esIcon('search', 'entity-search-icon');
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;"><i class="icon-spinner icon-spin"></i></span>';
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
break;
@@ -1587,11 +1607,11 @@
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') + '"><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 += '<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 += '</div>';
html += '<span class="mpr-info-wrapper" data-details="' + this.escapeAttr(tooltipContent) + '">';
html += '<span class="mpr-icon icon-info link"></span>';
html += this.esIcon('info');
html += '</span>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
@@ -1613,7 +1633,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') + '"><i class="icon-plus"></i></button>';
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '">' + this.esIcon('add') + '</button>';
html += '</div>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
@@ -1681,7 +1701,7 @@
html += '</div>';
}
html += '<div class="combination-groups-container">';
html += '<span class="combination-loading"><i class="icon-spinner icon-spin"></i> ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
html += '<span class="combination-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
html += '</div>';
html += '</div>';
// Store mode along with attributes: { mode: 'products'|'combinations', attributes: { groupId: [valueIds] } }
@@ -1727,28 +1747,14 @@
}
},
getSortIconClass: function(sortBy, sortDir) {
var isAsc = (sortDir === 'ASC');
getSortIconName: function(sortBy, sortDir) {
switch (sortBy) {
case 'name':
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';
return 'sort_by_alpha';
case 'random':
return 'icon-random';
return 'shuffle';
default:
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
return 'sort';
}
},
@@ -1793,7 +1799,7 @@
$btn.attr('data-sort', newSort);
$btn.attr('data-dir', newDir);
$btn.attr('title', newLabel + ' ' + (newDir === 'DESC' ? '↓' : '↑'));
$btn.find('i').attr('class', this.getSortIconClass(newSort, newDir));
$btn.find('i').replaceWith(this.esIcon(this.getSortIconName(newSort, newDir)));
},
// Validation
@@ -1833,7 +1839,7 @@
// Add error message after header
var $error = $('<div>', {
class: 'trait-validation-error',
html: '<i class="icon-warning"></i> ' + message
html: this.esIcon('warning') + ' ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
@@ -1852,8 +1858,7 @@
clearValidationError: function() {
this.$wrapper.removeClass('has-validation-error');
this.$wrapper.find('.trait-validation-error').remove();
},
}
};
})(jQuery);

View File

@@ -93,13 +93,13 @@
$select.addClass('method-select-hidden');
var $selectedOption = $select.find('option:selected');
var selectedIcon = $selectedOption.data('icon') || 'icon-caret-down';
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
var selectedLabel = $selectedOption.text();
var triggerHtml = '<div class="method-dropdown-trigger">';
triggerHtml += '<i class="' + this.escapeAttr(selectedIcon) + ' method-trigger-icon"></i>';
triggerHtml += this.esIcon(selectedIcon, 'method-trigger-icon');
triggerHtml += '<span class="method-trigger-label">' + this.escapeHtml(selectedLabel) + '</span>';
triggerHtml += '<i class="icon-caret-down method-trigger-caret"></i>';
triggerHtml += this.esIcon('arrow_drop_down', 'method-trigger-caret');
triggerHtml += '</div>';
var $trigger = $(triggerHtml);
@@ -127,10 +127,10 @@
*/
updateMethodTrigger: function($select, $trigger) {
var $selectedOption = $select.find('option:selected');
var selectedIcon = $selectedOption.data('icon') || 'icon-caret-down';
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
var selectedLabel = $selectedOption.text();
$trigger.find('.method-trigger-icon').attr('class', selectedIcon + ' method-trigger-icon');
$trigger.find('.method-trigger-icon').replaceWith(this.esIcon(selectedIcon, 'method-trigger-icon'));
$trigger.find('.method-trigger-label').text(selectedLabel);
},
@@ -194,16 +194,16 @@
// Render ungrouped options first
$select.children('option').each(function() {
var $el = $(this);
var icon = $el.data('icon') || 'icon-asterisk';
var icon = $el.data('icon') || 'star';
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 += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
html += self.esIcon(icon, 'method-item-icon');
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += '<i class="icon-check method-item-check"></i>';
html += self.esIcon('check', 'method-item-check');
}
html += '</div>';
});
@@ -219,16 +219,16 @@
$optgroup.children('option').each(function() {
var $el = $(this);
var icon = $el.data('icon') || 'icon-cog';
var icon = $el.data('icon') || 'settings';
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 += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
html += self.esIcon(icon, 'method-item-icon');
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += '<i class="icon-check method-item-check"></i>';
html += self.esIcon('check', 'method-item-check');
}
html += '</div>';
});
@@ -384,13 +384,13 @@
type: 'button',
class: 'comb-toolbar-btn comb-select-all',
title: trans.select_all || 'Select all',
html: '<i class="icon-check-square-o"></i>'
html: self.esIcon('check_box')
}));
$toolbar.append($('<button>', {
type: 'button',
class: 'comb-toolbar-btn comb-select-none',
title: trans.clear || 'Clear',
html: '<i class="icon-square-o"></i>'
html: self.esIcon('check_box_outline_blank')
}));
$toolbar.append($('<input>', {
type: 'text',
@@ -404,7 +404,7 @@
});
$valuesContainer.append($('<span>', {
class: 'comb-attr-loading',
html: '<i class="icon-spinner icon-spin"></i>'
html: self.esIcon('progress_activity', 'es-spin')
}));
$groupDiv.append($groupHeader);
@@ -584,10 +584,15 @@
if (helpContent) {
var $infoWrapper = $('<span>', {
class: 'mpr-info-wrapper',
'data-tooltip': helpContent
'data-details': helpContent
});
$infoWrapper.append($('<i>', { class: 'material-icons', text: 'info' }));
$infoWrapper.append($(this.esIcon('info')));
$placeholder.append($infoWrapper);
// Let prestashop-admin info-tooltip.js handle this element
if (window.MPRInfoTooltip) {
window.MPRInfoTooltip.init();
}
}
},
@@ -846,7 +851,7 @@
$wrapper.addClass('selector-locked');
if (!$wrapper.find('.lock-indicator').length) {
var lockHtml = '<span class="mpr-info-wrapper lock-indicator">' +
'<i class="icon-lock"></i>' +
this.esIcon('lock') +
'<span class="mpr-tooltip">' +
(trans.remove_excludes_first || 'Remove all exceptions to change selection type') +
'</span>' +

View File

@@ -37,7 +37,7 @@
$countValue.text(total);
} else {
// Fallback: set HTML with icon
$totalBadge.html('<i class="icon-eye"></i> <span class="count-value">' + total + '</span>');
$totalBadge.html(self.esIcon('visibility') + ' <span class="count-value">' + total + '</span>');
}
$totalBadge.show();
} else {
@@ -91,7 +91,7 @@
// 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"><i class="icon-times"></i></button>';
html += '<button type="button" class="preview-close">' + this.esIcon('close') + '</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"><i class="icon-plus"></i></button>';
html += '<button type="button" class="btn-load-more">' + self.esIcon('add') + '</button>';
html += '</div>';
html += '</div>';
}
@@ -181,7 +181,7 @@
if ($btn.hasClass('loading')) return;
$btn.addClass('loading');
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
$btn.find('i').replaceWith(self.esIcon('progress_activity', 'es-spin'));
$select.prop('disabled', true);
// Get selected load count
@@ -192,8 +192,23 @@
});
}
// Position popover relative to badge (handles viewport overflow)
this.positionPopover($popover, $badge);
// Position popover below badge
var badgeOffset = $badge.offset();
var badgeHeight = $badge.outerHeight();
var badgeWidth = $badge.outerWidth();
var popoverWidth = $popover.outerWidth();
var leftPos = badgeOffset.left + (badgeWidth / 2) - (popoverWidth / 2);
var minLeft = 10;
var maxLeft = $(window).width() - popoverWidth - 10;
leftPos = Math.max(minLeft, Math.min(leftPos, maxLeft));
$popover.css({
position: 'absolute',
top: badgeOffset.top + badgeHeight + 8,
left: leftPos,
zIndex: 10000
});
// Show with transition
$popover.addClass('show');
@@ -201,61 +216,6 @@
return $popover;
},
/**
* Position a popover relative to a trigger element.
* Handles horizontal and vertical viewport overflow.
* @param {jQuery} $popover - The popover element
* @param {jQuery} $trigger - The trigger element to position against
* @param {number} [zIndex=10000] - Optional z-index
*/
positionPopover: function($popover, $trigger, zIndex) {
var triggerRect = $trigger[0].getBoundingClientRect();
var scrollTop = $(window).scrollTop();
var scrollLeft = $(window).scrollLeft();
var popoverWidth = $popover.outerWidth();
var popoverHeight = $popover.outerHeight();
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var gap = 8;
// Horizontal: center on trigger, then clamp to viewport
var left = triggerRect.left + scrollLeft + (triggerRect.width / 2) - (popoverWidth / 2);
left = Math.max(10, Math.min(left, windowWidth - popoverWidth - 10));
// Vertical: prefer below, flip above if it would overflow
var top;
var positionAbove = false;
if (triggerRect.bottom + popoverHeight + gap > windowHeight - 10) {
// Position above the trigger
top = triggerRect.top + scrollTop - popoverHeight - gap;
positionAbove = true;
} else {
// Position below the trigger
top = triggerRect.bottom + scrollTop + gap;
}
$popover.css({
position: 'absolute',
top: top,
left: left,
zIndex: zIndex || 10000
});
// Toggle arrow direction class
$popover.toggleClass('position-above', positionAbove);
// Determine horizontal arrow position class
var triggerCenter = triggerRect.left + (triggerRect.width / 2);
var popoverLeft = left;
var popoverCenter = popoverLeft + (popoverWidth / 2);
$popover.removeClass('position-left position-right');
if (triggerCenter - popoverLeft < popoverWidth * 0.3) {
$popover.addClass('position-right');
} else if (triggerCenter - popoverLeft > popoverWidth * 0.7) {
$popover.addClass('position-left');
}
},
/**
* Update popover after loading more items
*/
@@ -276,7 +236,7 @@
// Reset button state
$btn.removeClass('loading');
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$btn.find('i').replaceWith(this.esIcon('add'));
$select.prop('disabled', false);
// Update remaining count
@@ -326,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"><i class="material-icons">inventory_2</i></div>';
html += '<div class="preview-item-icon">' + self.esIcon('inventory_2') + '</div>';
}
// Info section
@@ -418,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"><i class="icon-spinner icon-spin"></i></div>');
$list.append('<div class="filter-loading-overlay">' + this.esIcon('progress_activity', 'es-spin') + '</div>');
}
} else {
$list.removeClass('filtering');
@@ -470,7 +430,7 @@
var $select = $controls.find('.load-more-select');
$btn.removeClass('loading');
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$btn.find('i').replaceWith(self.esIcon('add'));
$select.prop('disabled', false);
$controls.find('.remaining-count').text(remaining);
@@ -493,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"><i class="icon-plus"></i></button>';
footerHtml += '<button type="button" class="btn-load-more">' + self.esIcon('add') + '</button>';
footerHtml += '</div>';
footerHtml += '</div>';
@@ -511,7 +471,7 @@
if ($btn.hasClass('loading')) return;
$btn.addClass('loading');
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
$btn.find('i').replaceWith(self.esIcon('progress_activity', 'es-spin'));
$select.prop('disabled', true);
var loadCount = parseInt($select.val(), 10) || 20;
@@ -783,7 +743,7 @@
var $controls = $btn.closest('.load-more-controls');
var $select = $controls.find('.load-more-select');
$btn.removeClass('loading');
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$btn.find('i').replaceWith(self.esIcon('add'));
$select.prop('disabled', false);
}
});
@@ -932,7 +892,7 @@
var $controls = $btn.closest('.load-more-controls');
var $select = $controls.find('.load-more-select');
$btn.removeClass('loading');
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$btn.find('i').replaceWith(self.esIcon('add'));
$select.prop('disabled', false);
}
});
@@ -1220,144 +1180,6 @@
});
},
// =========================================================================
// FILTER VALUE PREVIEW (individual attribute/feature value chip)
// =========================================================================
/**
* Show preview popover for a specific filter value (attribute or feature value)
* @param {jQuery} $badge - The preview button on the chip
* @param {number} valueId - The attribute/feature value ID
* @param {string} valueType - 'attribute' or 'feature'
* @param {string} valueName - Display name of the value
* @param {number} groupId - The parent group ID
*/
showFilterValuePreviewPopover: function($badge, valueId, valueType, valueName, groupId) {
var self = this;
this.hidePreviewPopover();
$badge.addClass('popover-open loading');
this.$activeBadge = $badge;
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewFilterValueProducts',
trait: 'EntitySelector',
value_id: valueId,
value_type: valueType,
group_id: groupId,
limit: 10
},
success: function(response) {
$badge.removeClass('loading');
if (response.success) {
self.createPreviewPopover({
$badge: $badge,
items: response.items || [],
totalCount: response.count || 0,
hasMore: response.hasMore || false,
entityLabel: 'products',
previewType: 'filter-value',
context: { valueId: valueId, valueType: valueType, groupId: groupId, valueName: valueName },
onLoadMore: function($btn) {
self.loadMoreFilterValueItems($btn);
},
onFilter: function(query) {
self.filterFilterValueItems(query);
}
});
} else {
$badge.removeClass('popover-open');
self.$activeBadge = null;
}
},
error: function() {
$badge.removeClass('loading popover-open');
self.$activeBadge = null;
}
});
},
/**
* AJAX filter handler for filter value preview
*/
filterFilterValueItems: function(query) {
var self = this;
var ctx = this.previewContext;
if (!ctx || !ctx.valueId) {
self.showFilterLoading(false);
return;
}
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewFilterValueProducts',
trait: 'EntitySelector',
value_id: ctx.valueId,
value_type: ctx.valueType,
group_id: ctx.groupId,
filter: query,
limit: 20
},
success: function(response) {
self.updatePreviewPopoverFiltered(response);
},
error: function() {
self.showFilterLoading(false);
}
});
},
loadMoreFilterValueItems: function($btn) {
var self = this;
var ctx = this.previewContext;
if (!ctx || !ctx.valueId) return;
var loadCount = this.previewLoadCount || 20;
var ajaxData = {
ajax: 1,
action: 'previewFilterValueProducts',
trait: 'EntitySelector',
value_id: ctx.valueId,
value_type: ctx.valueType,
group_id: ctx.groupId,
limit: self.previewLoadedCount + loadCount
};
if (self.previewCurrentFilter) {
ajaxData.filter = self.previewCurrentFilter;
}
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: ajaxData,
success: function(response) {
if (response.success) {
self.previewTotalCount = response.count;
self.updatePreviewPopover(response.items || [], response.hasMore);
}
},
error: function() {
$btn.removeClass('loading');
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
}
});
},
// =========================================================================
// CATEGORY ITEMS PREVIEW (products/pages in a category)
// =========================================================================
@@ -1425,7 +1247,7 @@
var isProducts = (ctx.entityType === 'categories');
var action = isProducts ? 'previewCategoryProducts' : 'previewCategoryPages';
$btn.prop('disabled', true).find('i').addClass('icon-spin');
$btn.prop('disabled', true).find('i').addClass('es-spin');
$.ajax({
url: this.config.ajaxUrl,
@@ -1441,7 +1263,7 @@
query: this.previewFilterQuery || ''
},
success: function(response) {
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
$btn.prop('disabled', false).find('i').removeClass('es-spin');
if (response.success && response.items) {
self.appendPreviewItems(response.items);
@@ -1453,7 +1275,7 @@
}
},
error: function() {
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
$btn.prop('disabled', false).find('i').removeClass('es-spin');
}
});
},
@@ -1513,13 +1335,13 @@
html += '<div class="pattern-preview-modal">';
html += '<div class="pattern-preview-header">';
html += '<span class="pattern-preview-title">';
html += '<i class="icon-eye"></i> ' + (trans.preview || 'Preview') + ': <code>' + this.escapeHtml(pattern) + '</code>';
html += this.esIcon('visibility') + ' ' + (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"><i class="icon-times"></i></button>';
html += '<button type="button" class="pattern-preview-close">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '<div class="pattern-preview-content">';
html += '<div class="pattern-preview-loading"><i class="icon-spinner icon-spin"></i> ' + (trans.loading || 'Loading...') + '</div>';
html += '<div class="pattern-preview-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
@@ -1650,7 +1472,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').attr('class') || 'icon-cube';
var icon = $tab.find('.tab-label').prev('i').text() || 'widgets';
var label = $tab.find('.tab-label').text() || blockType;
summaryItems.push({
@@ -1675,7 +1497,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 += '<i class="' + self.escapeAttr(item.icon) + '"></i>';
popoverHtml += this.esIcon(item.icon);
popoverHtml += '<span class="summary-item-label">' + self.escapeHtml(item.label) + '</span>';
popoverHtml += '<span class="summary-item-count">' + item.count + '</span>';
popoverHtml += '</li>';
@@ -1695,9 +1517,28 @@
self.switchToBlock(blockType);
});
// Position popover relative to badge (handles viewport overflow)
// Position popover
$('body').append($popover);
this.positionPopover($popover, $badge);
var badgeOffset = $badge.offset();
var badgeHeight = $badge.outerHeight();
var popoverWidth = $popover.outerWidth();
$popover.css({
position: 'absolute',
top: badgeOffset.top + badgeHeight + 5,
left: badgeOffset.left - (popoverWidth / 2) + ($badge.outerWidth() / 2),
zIndex: 10000
});
// Adjust if off screen
var windowWidth = $(window).width();
var popoverRight = $popover.offset().left + popoverWidth;
if (popoverRight > windowWidth - 10) {
$popover.css('left', windowWidth - popoverWidth - 10);
}
if ($popover.offset().left < 10) {
$popover.css('left', 10);
}
$popover.hide().fadeIn(150);
},
@@ -1729,10 +1570,10 @@
}
popoverHtml += this.escapeHtml(countryName) + ' - ' + (trans.holidays || 'Holidays');
popoverHtml += '</span>';
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
popoverHtml += '<button type="button" class="popover-close">' + this.esIcon('close') + '</button>';
popoverHtml += '</div>';
popoverHtml += '<div class="popover-body">';
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '<div class="holiday-preview-loading">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '</div>';
popoverHtml += '</div>';
@@ -1819,7 +1660,7 @@
$popover.find('.popover-body').html(listHtml);
} else {
var noDataHtml = '<div class="holiday-preview-empty">';
noDataHtml += '<i class="material-icons">event_busy</i>';
noDataHtml += self.esIcon('event_busy');
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
noDataHtml += '</div>';
$popover.find('.popover-body').html(noDataHtml);
@@ -1836,7 +1677,7 @@
},
error: function() {
var errorHtml = '<div class="holiday-preview-empty">';
errorHtml += '<i class="material-icons">error_outline</i>';
errorHtml += self.esIcon('error');
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
errorHtml += '</div>';
$popover.find('.popover-body').html(errorHtml);
@@ -1864,15 +1705,15 @@
// Create popover HTML with placeholder title (will update after AJAX)
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
popoverHtml += '<div class="popover-header">';
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 += '<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 += '</div>';
popoverHtml += '<div class="popover-filter">';
popoverHtml += '<i class="material-icons">search</i>';
popoverHtml += this.esIcon('search');
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"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '<div class="holiday-preview-loading">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
popoverHtml += '</div>';
popoverHtml += '</div>';
@@ -2060,7 +1901,7 @@
$popover.find('.popover-title').html('0 ' + (trans.holidays || 'Holidays'));
var noDataHtml = '<div class="holiday-preview-empty">';
noDataHtml += '<i class="material-icons">event_busy</i>';
noDataHtml += self.esIcon('event_busy');
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
noDataHtml += '</div>';
$popover.find('.popover-body').html(noDataHtml);
@@ -2077,10 +1918,10 @@
},
error: function() {
// Update header for error state
$popover.find('.popover-title').html('<i class="material-icons">error_outline</i> ' + (trans.error || 'Error'));
$popover.find('.popover-title').html(self.esIcon('error') + ' ' + (trans.error || 'Error'));
var errorHtml = '<div class="holiday-preview-empty">';
errorHtml += '<i class="material-icons">error_outline</i>';
errorHtml += self.esIcon('error');
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
errorHtml += '</div>';
$popover.find('.popover-body').html(errorHtml);

View File

@@ -40,11 +40,6 @@
sort_dir: this.currentSort ? this.currentSort.dir : 'ASC'
};
// Add product_selection_level if set
if (this.config.productSelectionLevel && this.config.productSelectionLevel !== 'product') {
requestData.product_selection_level = this.config.productSelectionLevel;
}
// Add refine query if present
if (this.refineQuery) {
requestData.refine = this.refineQuery;
@@ -366,14 +361,12 @@
var html = '';
if (visibleResults.length === 0 && !appendMode) {
html = '<div class="no-results"><i class="icon-search"></i> ' + (trans.no_results || 'No results found') + '</div>';
html = '<div class="no-results">' + this.esIcon('search') + ' ' + (trans.no_results || 'No results found') + '</div>';
} else {
visibleResults.forEach(function(item) {
var isSelected = selectedIds.indexOf(String(item.id)) !== -1;
var itemClass = 'dropdown-item' + (isSelected ? ' selected' : '');
if (item.type === 'product') itemClass += ' result-item-product';
if (item.is_combination) itemClass += ' is-combination';
if (item.is_parent) itemClass += ' is-parent-product';
html += '<div class="' + itemClass + '" ';
html += 'data-id="' + self.escapeAttr(item.id) + '" ';
@@ -381,30 +374,29 @@
if (item.image) html += ' data-image="' + self.escapeAttr(item.image) + '"';
if (item.subtitle) html += ' data-subtitle="' + self.escapeAttr(item.subtitle) + '"';
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
if (item.attributes) html += ' data-attributes="' + self.escapeAttr(item.attributes) + '"';
html += '>';
html += '<span class="result-checkbox"><i class="icon-check"></i></span>';
html += '<span class="result-checkbox">' + self.esIcon('check') + '</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;"><i class="icon-flag"></i></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;">' + self.esIcon('flag') + '</span></div>';
} else if (item.image) {
html += '<div class="result-image"><img src="' + self.escapeAttr(item.image) + '" alt=""></div>';
} else {
// Entity-specific icons
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>';
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>';
}
html += '<div class="result-info">';
@@ -596,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 += '<i class="icon-clock-o"></i>';
html += this.esIcon('schedule');
html += '<span class="history-query">' + this.escapeHtml(query) + '</span>';
html += '<button type="button" class="btn-delete-history" title="' + (trans.remove || 'Remove') + '">';
html += '<i class="icon-times"></i>';
html += this.esIcon('close');
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"><i class="icon-spinner icon-spin"></i> ' +
$results.html('<div class="tree-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' +
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 += '<i class="icon-plus-square-o"></i> ' + this.escapeHtml(trans.expand_all || 'Expand all');
html += this.esIcon('add_box') + ' ' + 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 += '<i class="icon-minus-square-o"></i> ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
html += this.esIcon('indeterminate_check_box') + ' ' + 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"><i class="icon-caret-down"></i></span>';
html += '<span class="tree-toggle">' + self.esIcon('arrow_drop_down') + '</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 += '<i class="icon-check-square-o"></i>';
html += self.esIcon('check_box');
html += '</button>';
} else {
html += '<span class="tree-toggle tree-leaf"></span>';
}
// Checkbox indicator
html += '<span class="tree-checkbox"><i class="icon-check"></i></span>';
html += '<span class="tree-checkbox">' + self.esIcon('check') + '</span>';
// Category icon
html += '<span class="tree-icon"><i class="icon-folder"></i></span>';
html += '<span class="tree-icon">' + self.esIcon('folder') + '</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 += '<i class="icon-eye"></i> ' + itemCount;
html += self.esIcon('visibility') + ' ' + itemCount;
html += '</span>';
}
@@ -346,10 +346,10 @@
});
if (isParentSelected && allChildrenSelected) {
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
} else {
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.attr('title', trans.select_with_children || 'Select with all children');
}
});

View File

@@ -19,6 +19,97 @@
// 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 = {
@@ -60,18 +151,62 @@
.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': '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'
'products': 'shopping_cart',
'categories': 'folder_open',
'manufacturers': 'business',
'suppliers': 'local_shipping',
'attributes': 'list_alt',
'features': 'label',
'cms': 'description',
'cms_categories': 'folder'
};
return icons[entityType] || 'icon-cube';
return icons[entityType] || 'widgets';
},
getEntityTypeLabel: function(entityType) {
@@ -116,7 +251,7 @@
this.$wrapper.find('.trait-validation-error').remove();
var $error = $('<div>', {
class: 'trait-validation-error',
html: '<i class="icon-warning"></i> ' + message
html: this.esIcon('warning') + ' ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
$('html, body').animate({ scrollTop: this.$wrapper.offset().top - 100 }, 300);

View File

@@ -297,12 +297,12 @@
// Create toast HTML
var html = '<div class="es-validation-toast">';
html += '<div class="es-toast-icon"><i class="icon-exclamation-triangle"></i></div>';
html += '<div class="es-toast-icon">' + this.esIcon('warning') + '</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"><i class="icon-times"></i></button>';
html += '<button type="button" class="es-toast-close">' + this.esIcon('close') + '</button>';
html += '</div>';
var $toast = $(html);