refactor: entity selector full overhaul — Mar 2026
- Unified _setBadgeCount for ALL badge updates - target-conditions-trait → entity-selector-trait - target-* → es-* class rename (20+ classes) - SCSS recompiled: zero duplicate selectors - CSS transitions replace jQuery slideDown/slideUp - Serialize cache, method swap cache - Badge: no-matches gray, consistent hover, no blending - Inline condition count always visible - Preview popover refreshes in-place on sort change - Categories add chips immediately - Entity type icons on chips - Consistent info_outline icons via buildHelpIcon - Method dropdown text clipping fix (line-height) - mpr-input-compact on all inputs - Dropdown padding fixed in SCSS source - Chips wrapper: same container always - Reusable helpers: _buildEmptyState, _buildSearchBoxHtml, _buildInfoTooltip - Asset path: uses $this->module->getPathUri() not reflection - Debug logs removed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
2
assets/js/admin/entity-selector.min.js
vendored
2
assets/js/admin/entity-selector.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -29,13 +29,15 @@
|
||||
|
||||
addSelection: function($picker, id, name, data) {
|
||||
this.addSelectionNoUpdate($picker, id, name, data);
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
this.updateChipsVisibility($chips);
|
||||
if (this.config.mode !== 'single') {
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
this.updateChipsVisibility($chips);
|
||||
}
|
||||
},
|
||||
|
||||
addSelectionNoUpdate: function($picker, id, name, data) {
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
var $block = $picker.closest('.target-block');
|
||||
var $block = $picker.closest('.es-block');
|
||||
|
||||
// Check for global single mode (only ONE item across ALL entity types)
|
||||
var globalMode = this.config.mode || 'multi';
|
||||
@@ -47,9 +49,6 @@
|
||||
if (this.$dropdown) {
|
||||
this.$dropdown.find('.dropdown-item.selected, .tree-item.selected').removeClass('selected');
|
||||
}
|
||||
// Clear tab badges (since we're clearing other blocks)
|
||||
this.$wrapper.find('.target-block-tab .tab-badge').remove();
|
||||
this.$wrapper.find('.target-block-tab').removeClass('has-data');
|
||||
} else {
|
||||
// Check if this block is in per-block single mode
|
||||
var blockMode = $block.data('mode') || 'multi';
|
||||
@@ -68,6 +67,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove empty state placeholder
|
||||
var hadEmpty = $chips.find('.chips-empty-state').length > 0;
|
||||
$chips.find('.chips-empty-state').remove();
|
||||
|
||||
// Check if this is a country entity (for flag and holiday preview)
|
||||
var blockType = $block.data('blockType') || '';
|
||||
var searchEntity = $picker.attr('data-search-entity') || blockType;
|
||||
@@ -79,21 +82,34 @@
|
||||
}
|
||||
html += '>';
|
||||
|
||||
// Country: show flag
|
||||
// Icon: flag for countries, image if available, or entity type icon
|
||||
if (isCountry && data && data.iso_code) {
|
||||
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + this.escapeAttr(data.iso_code.toLowerCase()) + '.png" alt="' + this.escapeAttr(data.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';">' + this.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '</span>';
|
||||
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + this.escapeAttr(data.iso_code.toLowerCase()) + '.png" alt="' + this.escapeAttr(data.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><i class="icon-flag flag-fallback" style="display:none;"></i></span>';
|
||||
} else if (data && data.image) {
|
||||
html += '<span class="chip-icon"><img src="' + this.escapeAttr(data.image) + '" alt=""></span>';
|
||||
} else {
|
||||
// Entity type icon from block config
|
||||
var blockIcon = '';
|
||||
if (this.config && this.config.blocks) {
|
||||
var bt = $block.data('blockType') || '';
|
||||
var blockDef = this.config.blocks[bt];
|
||||
if (blockDef && blockDef.icon) {
|
||||
blockIcon = blockDef.icon;
|
||||
}
|
||||
}
|
||||
if (blockIcon) {
|
||||
html += '<span class="chip-icon"><i class="' + this.escapeAttr(blockIcon) + '"></i></span>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<span class="chip-name">' + this.escapeHtml(name) + '</span>';
|
||||
|
||||
// Country: add holiday preview button
|
||||
if (isCountry) {
|
||||
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays">' + this.esIcon('visibility') + '</button>';
|
||||
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
|
||||
}
|
||||
|
||||
html += '<button type="button" class="chip-remove" title="Remove">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
|
||||
html += '</span>';
|
||||
|
||||
$chips.append(html);
|
||||
@@ -112,19 +128,25 @@
|
||||
var $allChips = $chips.find('.entity-chip');
|
||||
var totalCount = $allChips.length;
|
||||
|
||||
// If no chips, remove the wrapper entirely
|
||||
var $existingWrapper = $chips.closest('.chips-wrapper');
|
||||
// Ensure wrapper always exists
|
||||
this.ensureChipsWrapper($chips);
|
||||
|
||||
if (totalCount === 0) {
|
||||
if ($existingWrapper.length) {
|
||||
// Move chips out of wrapper before removing
|
||||
$existingWrapper.before($chips);
|
||||
$existingWrapper.remove();
|
||||
// Show empty state, hide toolbar
|
||||
var placeholder = $chips.data('placeholder') || trans.no_items_selected || 'No items selected';
|
||||
if (!$chips.find('.chips-empty-state').length) {
|
||||
$chips.html('<span class="chips-empty-state">' + self.escapeHtml(placeholder) + '</span>');
|
||||
}
|
||||
var $wrapper = $chips.closest('.chips-wrapper');
|
||||
if ($wrapper.length) {
|
||||
$wrapper.find('.chips-toolbar').removeClass('has-chips').hide();
|
||||
$wrapper.find('.chips-load-more').hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure chips wrapper structure exists
|
||||
this.ensureChipsWrapper($chips);
|
||||
// Has chips — show toolbar, remove empty state
|
||||
$chips.find('.chips-empty-state').remove();
|
||||
|
||||
var $wrapper = $chips.closest('.chips-wrapper');
|
||||
var $toolbar = $wrapper.find('.chips-toolbar');
|
||||
@@ -160,7 +182,7 @@
|
||||
});
|
||||
|
||||
// Update toolbar (always show when we have chips)
|
||||
$toolbar.addClass('has-chips');
|
||||
$toolbar.addClass('has-chips').show();
|
||||
this.updateChipsToolbar($toolbar, totalCount, filteredCount, searchTerm);
|
||||
|
||||
// Update load more select dropdown
|
||||
@@ -183,7 +205,7 @@
|
||||
'<button type="button" class="btn-collapse-chips" style="' +
|
||||
'background:transparent;border:1px solid #dee2e6;border-radius:4px;' +
|
||||
'padding:0.25rem 0.75rem;font-size:12px;color:#6c757d;cursor:pointer;">' +
|
||||
this.esIcon('expand_less') + ' ' + collapseText +
|
||||
'<i class="icon-chevron-up"></i> ' + collapseText +
|
||||
'</button>'
|
||||
).show();
|
||||
} else {
|
||||
@@ -192,37 +214,44 @@
|
||||
},
|
||||
|
||||
ensureChipsWrapper: function($chips) {
|
||||
// Check if already wrapped
|
||||
if ($chips.closest('.chips-wrapper').length) {
|
||||
// Always create wrapper if missing
|
||||
var $wrapper = $chips.closest('.chips-wrapper');
|
||||
if (!$wrapper.length) {
|
||||
$chips.wrap('<div class="chips-wrapper"></div>');
|
||||
$wrapper = $chips.closest('.chips-wrapper');
|
||||
$wrapper.prepend('<div class="chips-toolbar" style="display:none;"></div>');
|
||||
$wrapper.append('<div class="chips-load-more" style="display:none;"></div>');
|
||||
}
|
||||
|
||||
// Skip toolbar POPULATION for single mode
|
||||
if (this.config.mode === 'single') {
|
||||
return;
|
||||
}
|
||||
var $block = $chips.closest('.es-block');
|
||||
if ($block.data('mode') === 'single') {
|
||||
return;
|
||||
}
|
||||
|
||||
var trans = this.config.trans || {};
|
||||
var $picker = $chips.closest('.value-picker');
|
||||
// If toolbar already populated, nothing to do
|
||||
var $toolbar = $wrapper.find('.chips-toolbar');
|
||||
if ($toolbar.children().length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapper structure - integrated filter toolbar with sort
|
||||
var wrapperHtml = '<div class="chips-wrapper">' +
|
||||
'<div class="chips-toolbar">' +
|
||||
'<input type="text" class="chips-search-input" placeholder="' + (trans.filter_selected || 'Filter selected') + '...">' +
|
||||
'<select class="chips-sort-select" title="' + (trans.sort || 'Sort') + '">' +
|
||||
// Populate toolbar content
|
||||
var trans = this.config.trans || {};
|
||||
var toolbarHtml =
|
||||
'<input type="text" class="chips-search-input mpr-input-compact" placeholder="' + (trans.filter_selected || 'Filter selected') + '...">' +
|
||||
'<select class="chips-sort-select mpr-input-compact" title="' + (trans.sort || 'Sort') + '">' +
|
||||
'<option value="added">' + (trans.sort_added || 'Order added') + '</option>' +
|
||||
'<option value="name_asc">' + (trans.sort_name_asc || 'Name A-Z') + '</option>' +
|
||||
'<option value="name_desc">' + (trans.sort_name_desc || 'Name Z-A') + '</option>' +
|
||||
'</select>' +
|
||||
'<span class="chips-count"></span>' +
|
||||
'<button type="button" class="btn-chips-clear" title="' + (trans.clear_all || 'Clear all') + '">' +
|
||||
this.esIcon('delete') + ' <span class="clear-text">' + (trans.clear || 'Clear') + '</span>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="chips-load-more" style="display:none;"></div>' +
|
||||
'</div>';
|
||||
|
||||
var $wrapper = $(wrapperHtml);
|
||||
|
||||
// Insert wrapper before chips and move chips inside
|
||||
$chips.before($wrapper);
|
||||
$wrapper.find('.chips-toolbar').after($chips);
|
||||
$wrapper.append($wrapper.find('.chips-load-more'));
|
||||
'<i class="icon-trash"></i> <span class="clear-text">' + (trans.clear || 'Clear') + '</span>' +
|
||||
'</button>';
|
||||
$toolbar.html(toolbarHtml);
|
||||
|
||||
// Bind toolbar events
|
||||
this.bindChipsToolbarEvents($wrapper);
|
||||
@@ -361,16 +390,13 @@
|
||||
|
||||
loadExistingSelections: function() {
|
||||
var self = this;
|
||||
console.log('[EntitySelector] loadExistingSelections called for id:', this.config.id);
|
||||
|
||||
// Collect all entity IDs to load, grouped by entity type
|
||||
var entitiesToLoad = {}; // { entity_type: { ids: [], pickers: [] } }
|
||||
|
||||
console.log('[EntitySelector] Looking for .selection-group in wrapper:', this.$wrapper.length ? 'found' : 'NOT FOUND');
|
||||
this.$wrapper.find('.selection-group').each(function() {
|
||||
console.log('[EntitySelector] Found .selection-group, index:', $(this).data('groupIndex'));
|
||||
var $group = $(this);
|
||||
var $block = $group.closest('.target-block');
|
||||
var $block = $group.closest('.es-block');
|
||||
var blockType = $block.data('blockType');
|
||||
|
||||
// Load include values
|
||||
@@ -415,11 +441,9 @@
|
||||
|
||||
// Skip AJAX if no entities to load
|
||||
if (!hasEntities) {
|
||||
console.log('[EntitySelector] No entities to load, skipping AJAX');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[EntitySelector] Making bulk AJAX request for entities:', JSON.stringify(bulkRequest));
|
||||
|
||||
// Single bulk AJAX call for all entity types
|
||||
$.ajax({
|
||||
@@ -433,9 +457,7 @@
|
||||
entities: JSON.stringify(bulkRequest)
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('[EntitySelector] AJAX response:', response);
|
||||
if (!response.success || !response.entities) {
|
||||
console.log('[EntitySelector] Response failed or no entities');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -475,21 +497,27 @@
|
||||
}
|
||||
html += '>';
|
||||
|
||||
// Country: show flag
|
||||
// Icon: flag, image, or entity type icon
|
||||
if (isCountry && entity.iso_code) {
|
||||
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + self.escapeAttr(entity.iso_code.toLowerCase()) + '.png" alt="' + self.escapeAttr(entity.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';">' + self.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '</span>';
|
||||
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + self.escapeAttr(entity.iso_code.toLowerCase()) + '.png" alt="' + self.escapeAttr(entity.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><i class="icon-flag flag-fallback" style="display:none;"></i></span>';
|
||||
} else if (entity.image) {
|
||||
html += '<span class="chip-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
|
||||
} else {
|
||||
var bt = $block.data('blockType') || '';
|
||||
var blockDef = self.config.blocks && self.config.blocks[bt];
|
||||
if (blockDef && blockDef.icon) {
|
||||
html += '<span class="chip-icon"><i class="' + self.escapeAttr(blockDef.icon) + '"></i></span>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<span class="chip-name">' + self.escapeHtml(entity.name) + '</span>';
|
||||
|
||||
// Country: add holiday preview button
|
||||
if (isCountry) {
|
||||
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays">' + self.esIcon('visibility') + '</button>';
|
||||
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
|
||||
}
|
||||
|
||||
html += '<button type="button" class="chip-remove" title="Remove">' + self.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
|
||||
html += '</span>';
|
||||
|
||||
$loadingChip.replaceWith(html);
|
||||
@@ -508,7 +536,7 @@
|
||||
self.serializeAllBlocks();
|
||||
}
|
||||
|
||||
self.updateBlockStatus($picker.closest('.target-block'));
|
||||
self.updateBlockStatus($picker.closest('.es-block'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -530,30 +558,23 @@
|
||||
* Also shows loading placeholders for entity_search types
|
||||
*/
|
||||
collectPickerEntities: function($picker, blockType, entitiesToLoad) {
|
||||
console.log('[EntitySelector] collectPickerEntities called, blockType:', blockType, 'picker length:', $picker.length);
|
||||
if (!$picker.length) {
|
||||
console.log('[EntitySelector] Picker not found, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
|
||||
console.log('[EntitySelector] Looking for values-data input, found:', $dataInput.length);
|
||||
if (!$dataInput.length) {
|
||||
console.log('[EntitySelector] No data input found, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
var valueType = $picker.attr('data-value-type');
|
||||
var rawValue = $dataInput.val() || '[]';
|
||||
console.log('[EntitySelector] valueType:', valueType, 'rawValue:', rawValue);
|
||||
|
||||
var values = [];
|
||||
try {
|
||||
values = JSON.parse(rawValue);
|
||||
console.log('[EntitySelector] Parsed values:', values);
|
||||
} catch (e) {
|
||||
console.log('[EntitySelector] JSON parse error:', e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -583,7 +604,7 @@
|
||||
$chip.append($('<button>', {
|
||||
type: 'button',
|
||||
class: 'btn-remove-range',
|
||||
html: self.esIcon('close')
|
||||
html: '<i class="icon-times"></i>'
|
||||
}));
|
||||
|
||||
$chipsContainer.append($chip);
|
||||
@@ -636,7 +657,7 @@
|
||||
// Show loading placeholders with entity-specific icons
|
||||
values.forEach(function(id) {
|
||||
var html = '<span class="entity-chip entity-chip-loading" data-id="' + self.escapeAttr(id) + '">';
|
||||
html += '<span class="chip-icon">' + self.esIcon(entityIcon, 'es-spin-pulse') + '</span>';
|
||||
html += '<span class="chip-icon"><i class="' + entityIcon + ' icon-spin-pulse"></i></span>';
|
||||
html += '<span class="chip-name">Loading...</span>';
|
||||
html += '</span>';
|
||||
$chips.append(html);
|
||||
@@ -697,7 +718,7 @@
|
||||
$chip.append($('<button>', {
|
||||
type: 'button',
|
||||
class: 'btn-remove-range',
|
||||
html: self.esIcon('close')
|
||||
html: '<i class="icon-times"></i>'
|
||||
}));
|
||||
|
||||
$chipsContainer.append($chip);
|
||||
@@ -783,7 +804,7 @@
|
||||
self.serializeAllBlocks();
|
||||
}
|
||||
|
||||
self.updateBlockStatus($picker.closest('.target-block'));
|
||||
self.updateBlockStatus($picker.closest('.es-block'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -810,7 +831,7 @@
|
||||
html += '<span class="case-icon">' + (isCaseSensitive ? 'Aa' : 'aa') + '</span>';
|
||||
html += '</button>';
|
||||
html += '<span class="pattern-tag-text">' + this.escapeHtml(pattern) + '</span>';
|
||||
html += '<button type="button" class="btn-remove-pattern" title="' + this.escapeAttr(trans.remove_pattern || 'Remove pattern') + '">' + this.esIcon('delete') + '</button>';
|
||||
html += '<button type="button" class="btn-remove-pattern" title="' + this.escapeAttr(trans.remove_pattern || 'Remove pattern') + '"><i class="icon-trash"></i></button>';
|
||||
html += '</div>';
|
||||
$chipsContainer.append(html);
|
||||
},
|
||||
@@ -845,12 +866,11 @@
|
||||
var $countValue = $matchCount.find('.count-value');
|
||||
|
||||
// Get entity type from block
|
||||
var $block = $draftTag.closest('.target-block');
|
||||
var $block = $draftTag.closest('.es-block');
|
||||
var entityType = $block.data('blockType') || 'products';
|
||||
|
||||
// Show loading - keep eye icon, update count value
|
||||
$countValue.html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$matchCount.show();
|
||||
// Show loading state — keep existing count visible
|
||||
if ($matchCount.hasClass('no-matches')) { $matchCount.addClass('loading-count'); } $matchCount.show();
|
||||
|
||||
// Store pattern for click handler
|
||||
$matchCount.data('pattern', pattern);
|
||||
@@ -916,7 +936,7 @@
|
||||
|
||||
var method = $methodSelect.val();
|
||||
if (!method) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -940,16 +960,15 @@
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var $block = $row.closest('.target-block');
|
||||
var $block = $row.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
|
||||
// Show loading
|
||||
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$countEl.removeClass('clickable no-matches').show();
|
||||
// Show loading state — keep existing count visible
|
||||
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
|
||||
|
||||
// Store condition data on badge for popover
|
||||
$countEl.data('conditionData', {
|
||||
@@ -974,20 +993,13 @@
|
||||
success: function(response) {
|
||||
if (response && response.success) {
|
||||
var count = response.count || 0;
|
||||
$countEl.removeClass('no-matches clickable');
|
||||
if (count === 0) {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('no-matches').show();
|
||||
} else {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('clickable').show();
|
||||
}
|
||||
self._setBadgeCount($countEl, count);
|
||||
} else {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -996,6 +1008,7 @@
|
||||
* Fetch pattern match count via AJAX
|
||||
*/
|
||||
fetchPatternMatchCount: function($picker, pattern, $countEl) {
|
||||
var self = this;
|
||||
// Determine field type from method select
|
||||
// Check if we're in an exclude row first, then fall back to include
|
||||
var $excludeRow = $picker.closest('.exclude-row');
|
||||
@@ -1010,12 +1023,12 @@
|
||||
var field = method.indexOf('reference') !== -1 ? 'reference' : 'name';
|
||||
|
||||
// Get entity type from block
|
||||
var $block = $picker.closest('.target-block');
|
||||
var $block = $picker.closest('.es-block');
|
||||
var entityType = $block.data('blockType') || 'products';
|
||||
|
||||
// Show loading state
|
||||
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$countEl.removeClass('clickable no-matches').show();
|
||||
$countEl.html('<i class="icon-spinner icon-spin"></i>');
|
||||
$countEl.removeClass('clickable no-matches').addClass('loading-count').show();
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -1033,19 +1046,13 @@
|
||||
success: function(response) {
|
||||
if (response && response.success) {
|
||||
var count = response.count || 0;
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.removeClass('no-matches clickable').show();
|
||||
if (count === 0) {
|
||||
$countEl.addClass('no-matches');
|
||||
} else {
|
||||
$countEl.addClass('clickable');
|
||||
}
|
||||
self._setBadgeCount($countEl, count);
|
||||
} else {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1125,7 +1132,7 @@
|
||||
|
||||
var method = $methodSelect.val();
|
||||
if (!method) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1147,17 +1154,16 @@
|
||||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
|
||||
));
|
||||
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get block type
|
||||
var $block = $row.closest('.target-block');
|
||||
var $block = $row.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
|
||||
// Show loading
|
||||
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$countEl.removeClass('clickable no-matches').show();
|
||||
// Show loading state — keep existing count visible
|
||||
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
|
||||
|
||||
// Store condition data on badge for popover
|
||||
$countEl.data('conditionData', {
|
||||
@@ -1182,21 +1188,13 @@
|
||||
success: function(response) {
|
||||
if (response && response.success) {
|
||||
var count = response.count || 0;
|
||||
$countEl.removeClass('no-matches clickable');
|
||||
if (count === 0) {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('no-matches').show();
|
||||
} else {
|
||||
// Show count, make clickable for preview popover
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('clickable').show();
|
||||
}
|
||||
self._setBadgeCount($countEl, count);
|
||||
} else {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1228,7 +1226,7 @@
|
||||
*/
|
||||
updateGroupTotalCount: function($group) {
|
||||
var self = this;
|
||||
var $block = $group.closest('.target-block');
|
||||
var $block = $group.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
var $badge = $group.find('.group-header .group-count-badge');
|
||||
var $limitInput = $group.find('.group-modifier-limit');
|
||||
@@ -1244,7 +1242,7 @@
|
||||
}
|
||||
|
||||
// Show loading
|
||||
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
|
||||
$badge.html('<i class="icon-spinner icon-spin"></i>').show();
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -1262,13 +1260,15 @@
|
||||
var finalCount = response.final_count || 0;
|
||||
var excludeCount = response.exclude_count || 0;
|
||||
|
||||
// Update badge with eye icon and count
|
||||
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
|
||||
// Update badge with eye icon and count (custom HTML for exclude info)
|
||||
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
|
||||
if (excludeCount > 0) {
|
||||
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
|
||||
}
|
||||
$badge.html(badgeHtml);
|
||||
$badge.addClass('clickable').show();
|
||||
if ($badge.html() !== badgeHtml) {
|
||||
$badge.html(badgeHtml);
|
||||
}
|
||||
self._setBadgeCount($badge, finalCount);
|
||||
|
||||
// Store group data on badge for preview popover
|
||||
$badge.data('groupData', groupData);
|
||||
@@ -1286,12 +1286,12 @@
|
||||
$previewBadge.text(displayCount);
|
||||
}
|
||||
} else {
|
||||
$badge.hide().removeClass('clickable');
|
||||
self._setBadgeCount($badge, 0);
|
||||
$limitInput.attr('placeholder', '–');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$badge.hide();
|
||||
self._setBadgeCount($badge, 0);
|
||||
$limitInput.attr('placeholder', '–');
|
||||
}
|
||||
});
|
||||
@@ -1302,7 +1302,7 @@
|
||||
*/
|
||||
updateAllConditionCounts: function() {
|
||||
var self = this;
|
||||
this.$wrapper.find('.target-block.active .selection-group').each(function() {
|
||||
this.$wrapper.find('.es-block.active .selection-group').each(function() {
|
||||
self.updateGroupCounts($(this));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
init: function(options) {
|
||||
this.config = $.extend({
|
||||
id: 'target-conditions',
|
||||
id: 'entity-selector',
|
||||
name: 'target_conditions',
|
||||
namePrefix: 'target_',
|
||||
mode: 'multi', // Global mode: 'multi' or 'single'
|
||||
@@ -117,7 +117,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Global single mode - hide "Add Group" buttons
|
||||
// Global single mode - hide group management
|
||||
if (this.config.mode === 'single') {
|
||||
this.$wrapper.find('.btn-add-group').hide();
|
||||
this.$wrapper.find('.group-excludes').hide();
|
||||
@@ -132,6 +132,7 @@
|
||||
var $formGroup = this.$wrapper.closest('.form-group');
|
||||
$formGroup.addClass('condition-trait-fullwidth');
|
||||
$formGroup.find('.col-lg-offset-3').removeClass('col-lg-offset-3');
|
||||
} else {
|
||||
}
|
||||
|
||||
this.createDropdown();
|
||||
@@ -147,10 +148,14 @@
|
||||
|
||||
// Update counts on page load
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.updateTabBadges();
|
||||
self.updateAllConditionCounts();
|
||||
}, 100);
|
||||
if (this.config.mode === 'single') {
|
||||
setTimeout(function() { self._updateSingleModeBadges(); }, 100);
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
self.updateTabBadges();
|
||||
self.updateAllConditionCounts();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
observeNewSelects: function() {
|
||||
@@ -287,15 +292,15 @@
|
||||
});
|
||||
|
||||
// Tips box toggle handler
|
||||
$(document).on('click', '.target-tips-box .tips-header', function(e) {
|
||||
$(document).on('click', '.es-tips-box .tips-header', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).closest('.target-tips-box').toggleClass('expanded');
|
||||
$(this).closest('.es-tips-box').toggleClass('expanded');
|
||||
});
|
||||
|
||||
// Form submission validation for required target conditions
|
||||
$(document).on('submit', 'form', function(e) {
|
||||
var $form = $(this);
|
||||
if ($form.find('.target-conditions-trait[data-required]').length > 0) {
|
||||
if ($form.find('.entity-selector-trait[data-required]').length > 0) {
|
||||
if (!TargetConditions.validateAll()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
window._EntitySelectorMixins.dropdown = {
|
||||
|
||||
createDropdown: function() {
|
||||
this.$wrapper.find('.target-search-dropdown').remove();
|
||||
this.$wrapper.find('.es-search-dropdown').remove();
|
||||
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
var html = '<div class="target-search-dropdown view-list">';
|
||||
var html = '<div class="es-search-dropdown view-list">';
|
||||
|
||||
// Header with results count, actions, sort controls, view mode
|
||||
html += '<div class="dropdown-header">';
|
||||
@@ -26,10 +26,10 @@
|
||||
|
||||
// Select all / Clear buttons with keyboard shortcuts
|
||||
html += '<button type="button" class="btn-select-all" title="' + (trans.select_all || 'Select all visible') + '">';
|
||||
html += this.esIcon('check_box') + ' ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
|
||||
html += '<i class="icon-check-square-o"></i> ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
|
||||
html += '</button>';
|
||||
html += '<button type="button" class="btn-clear-selection" title="' + (trans.clear_selection || 'Clear selection') + '">';
|
||||
html += this.esIcon('check_box_outline_blank') + ' ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
|
||||
html += '<i class="icon-square-o"></i> ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
|
||||
html += '</button>';
|
||||
|
||||
// Sort controls - options with data-entities attribute for entity-specific filtering
|
||||
@@ -50,7 +50,7 @@
|
||||
html += '<option value="product_count" data-entities="categories,manufacturers,suppliers">' + (trans.sort_product_count || 'Products') + '</option>';
|
||||
html += '</select>';
|
||||
html += '<button type="button" class="btn-sort-dir" data-dir="ASC" title="Sort direction">';
|
||||
html += this.esIcon('sort_by_alpha');
|
||||
html += '<i class="icon-sort-alpha-asc"></i>';
|
||||
html += '</button>';
|
||||
|
||||
// View mode selector - Tree option always present, shown for categories
|
||||
@@ -69,19 +69,19 @@
|
||||
|
||||
// Refine search
|
||||
html += '<div class="refine-compact">';
|
||||
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '">' + this.esIcon('block') + '</button>';
|
||||
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '"><i class="icon-ban"></i></button>';
|
||||
html += '<input type="text" class="refine-input" placeholder="' + (trans.refine_short || 'Refine...') + '">';
|
||||
html += '<button type="button" class="btn-clear-refine" style="display:none;">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-refine" style="display:none;"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
// Filter toggle button
|
||||
html += '<button type="button" class="btn-toggle-filters" title="' + (trans.toggle_filters || 'Filters') + '">';
|
||||
html += this.esIcon('filter_list');
|
||||
html += '<i class="icon-filter"></i>';
|
||||
html += '</button>';
|
||||
|
||||
// History button
|
||||
html += '<button type="button" class="btn-show-history" title="' + (trans.recent_searches || 'Recent searches') + '">';
|
||||
html += this.esIcon('schedule');
|
||||
html += '<i class="icon-clock-o"></i>';
|
||||
html += '</button>';
|
||||
|
||||
html += '</div>'; // End dropdown-actions
|
||||
@@ -104,13 +104,13 @@
|
||||
html += '</div>';
|
||||
|
||||
html += '<button type="button" class="btn-clear-filters" title="' + (trans.clear_filters || 'Clear filters') + '">';
|
||||
html += this.esIcon('close');
|
||||
html += '<i class="icon-times"></i>';
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
|
||||
// Attribute/Feature filter toggles for products
|
||||
html += '<div class="filter-row filter-row-attributes" data-entity="products" style="display:none;">';
|
||||
html += '<span class="filter-row-label">' + this.esIcon('label') + ' ' + (trans.attributes || 'Attributes') + ':</span>';
|
||||
html += '<span class="filter-row-label"><i class="icon-tags"></i> ' + (trans.attributes || 'Attributes') + ':</span>';
|
||||
html += '<div class="filter-attributes-container"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-row filter-row-values filter-row-attr-values" data-type="attribute" style="display:none;">';
|
||||
@@ -118,7 +118,7 @@
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="filter-row filter-row-features" data-entity="products" style="display:none;">';
|
||||
html += '<span class="filter-row-label">' + this.esIcon('list') + ' ' + (trans.features || 'Features') + ':</span>';
|
||||
html += '<span class="filter-row-label"><i class="icon-list-ul"></i> ' + (trans.features || 'Features') + ':</span>';
|
||||
html += '<div class="filter-features-container"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-row filter-row-values filter-row-feat-values" data-type="feature" style="display:none;">';
|
||||
@@ -129,19 +129,19 @@
|
||||
html += '<div class="filter-row filter-row-entity-categories filter-row-multi" data-entity="categories" style="display:none;">';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
@@ -150,7 +150,7 @@
|
||||
html += '</div>';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-select-group">';
|
||||
html += '<span class="filter-select-label">' + this.esIcon('account_tree') + ' ' + (trans.depth || 'Depth') + ':</span>';
|
||||
html += '<span class="filter-select-label"><i class="icon-sitemap"></i> ' + (trans.depth || 'Depth') + ':</span>';
|
||||
html += '<select class="filter-depth-select">';
|
||||
html += '<option value="">' + (trans.all_levels || 'All levels') + '</option>';
|
||||
html += '<option value="1">' + (trans.level || 'Level') + ' 1 (' + (trans.root || 'Root') + ')</option>';
|
||||
@@ -162,7 +162,7 @@
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-has-products"> ' + (trans.has_products || 'Has products') + '</label>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-has-description"> ' + (trans.has_description || 'Has description') + '</label>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-has-image"> ' + (trans.has_image || 'Has image') + '</label>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -170,19 +170,19 @@
|
||||
html += '<div class="filter-row filter-row-entity-manufacturers filter-row-multi" data-entity="manufacturers" style="display:none;">';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
@@ -191,18 +191,18 @@
|
||||
html += '</div>';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-date-group">';
|
||||
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
|
||||
html += '<span class="filter-date-label"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
|
||||
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-date-group">';
|
||||
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
|
||||
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
|
||||
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -210,19 +210,19 @@
|
||||
html += '<div class="filter-row filter-row-entity-suppliers filter-row-multi" data-entity="suppliers" style="display:none;">';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
@@ -231,18 +231,18 @@
|
||||
html += '</div>';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-date-group">';
|
||||
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
|
||||
html += '<span class="filter-date-label"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
|
||||
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-date-group">';
|
||||
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
|
||||
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
|
||||
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -250,19 +250,19 @@
|
||||
html += '<div class="filter-row filter-row-entity-attributes filter-row-multi" data-entity="attributes" style="display:none;">';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
@@ -270,13 +270,13 @@
|
||||
html += '</div>';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-select-group">';
|
||||
html += '<span class="filter-select-label">' + this.esIcon('label') + ' ' + (trans.attribute_group || 'Group') + ':</span>';
|
||||
html += '<span class="filter-select-label"><i class="icon-tags"></i> ' + (trans.attribute_group || 'Group') + ':</span>';
|
||||
html += '<select class="filter-attribute-group-select">';
|
||||
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-is-color"> ' + (trans.color_only || 'Color attributes') + '</label>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -284,19 +284,19 @@
|
||||
html += '<div class="filter-row filter-row-entity-features filter-row-multi" data-entity="features" style="display:none;">';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
|
||||
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
|
||||
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
html += '</div>';
|
||||
html += '<div class="filter-range-group">';
|
||||
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
|
||||
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
|
||||
html += '<span class="filter-range-sep">-</span>';
|
||||
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
|
||||
@@ -304,13 +304,13 @@
|
||||
html += '</div>';
|
||||
html += '<div class="filter-subrow">';
|
||||
html += '<div class="filter-select-group">';
|
||||
html += '<span class="filter-select-label">' + this.esIcon('list') + ' ' + (trans.feature_group || 'Group') + ':</span>';
|
||||
html += '<span class="filter-select-label"><i class="icon-list-ul"></i> ' + (trans.feature_group || 'Group') + ':</span>';
|
||||
html += '<select class="filter-feature-group-select">';
|
||||
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-is-custom"> ' + (trans.custom_only || 'Custom values') + '</label>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -318,13 +318,13 @@
|
||||
html += '<div class="filter-row filter-row-entity-cms" data-entity="cms" style="display:none;">';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-indexable"> ' + (trans.indexable || 'Indexable') + '</label>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
// Entity-specific filters: CMS Categories
|
||||
html += '<div class="filter-row filter-row-entity-cms-categories" data-entity="cms_categories" style="display:none;">';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
// Entity-specific filters: Countries
|
||||
@@ -333,12 +333,12 @@
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-has-holidays"> ' + (trans.has_holidays || 'Has holidays') + '</label>';
|
||||
html += '<label class="filter-label"><input type="checkbox" class="filter-contains-states"> ' + (trans.contains_states || 'Has states') + '</label>';
|
||||
html += '<div class="filter-select-group">';
|
||||
html += '<span class="filter-select-label">' + this.esIcon('language') + ' ' + (trans.zone || 'Zone') + ':</span>';
|
||||
html += '<span class="filter-select-label"><i class="icon-globe"></i> ' + (trans.zone || 'Zone') + ':</span>';
|
||||
html += '<select class="filter-zone-select">';
|
||||
html += '<option value="">' + (trans.all_zones || 'All zones') + '</option>';
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>'; // End filter-panel
|
||||
@@ -373,8 +373,8 @@
|
||||
|
||||
// Right side: action buttons
|
||||
html += '<div class="dropdown-footer-right">';
|
||||
html += '<button type="button" class="dropdown-action-btn btn-cancel">' + this.esIcon('close') + ' ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
|
||||
html += '<button type="button" class="dropdown-action-btn btn-save">' + this.esIcon('check') + ' ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
|
||||
html += '<button type="button" class="dropdown-action-btn btn-cancel"><i class="icon-times"></i> ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
|
||||
html += '<button type="button" class="dropdown-action-btn btn-save"><i class="icon-check"></i> ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
@@ -35,18 +35,18 @@
|
||||
var self = this;
|
||||
|
||||
// Tab switching
|
||||
this.$wrapper.on('click', '.target-block-tab', function(e) {
|
||||
this.$wrapper.on('click', '.es-block-tab', function(e) {
|
||||
e.preventDefault();
|
||||
var blockType = $(this).data('blockType');
|
||||
self.switchToBlock(blockType);
|
||||
});
|
||||
|
||||
// Tab badge click for preview popover (toggle)
|
||||
this.$wrapper.on('click', '.target-block-tab .tab-badge', function(e) {
|
||||
this.$wrapper.on('click', '.es-block-tab .tab-badge', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var $tab = $(this).closest('.target-block-tab');
|
||||
var $tab = $(this).closest('.es-block-tab');
|
||||
var $badge = $(this);
|
||||
|
||||
if ($badge.hasClass('popover-open')) {
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
// Close popover when clicking outside
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.target-preview-popover').length &&
|
||||
if (!$(e.target).closest('.es-preview-popover').length &&
|
||||
!$(e.target).closest('.holiday-preview-popover').length &&
|
||||
!$(e.target).closest('.tab-badge').length &&
|
||||
!$(e.target).closest('.condition-match-count').length &&
|
||||
@@ -118,19 +118,18 @@
|
||||
|
||||
// Block-level collapse toggle (click on header)
|
||||
this.$wrapper.on('click', '.condition-trait-header', function(e) {
|
||||
if ($(e.target).closest('.target-block-tabs').length ||
|
||||
if ($(e.target).closest('.es-block-tabs').length ||
|
||||
$(e.target).closest('.trait-header-actions').length ||
|
||||
$(e.target).closest('.prestashop-switch').length ||
|
||||
$(e.target).closest('.trait-total-count').length) {
|
||||
return;
|
||||
}
|
||||
var $body = self.$wrapper.find('.condition-trait-body');
|
||||
$body.stop(true, true);
|
||||
if ($body.is(':visible')) {
|
||||
$body.slideUp(200);
|
||||
if ($body.hasClass('es-expanded')) {
|
||||
$body.removeClass('es-expanded');
|
||||
self.$wrapper.addClass('collapsed');
|
||||
} else {
|
||||
$body.slideDown(200);
|
||||
$body.addClass('es-expanded');
|
||||
self.$wrapper.removeClass('collapsed');
|
||||
}
|
||||
});
|
||||
@@ -139,14 +138,13 @@
|
||||
this.$wrapper.on('click', '.btn-toggle-blocks', function(e) {
|
||||
e.preventDefault();
|
||||
var $blocksContent = self.$wrapper.find('.entity-selector-blocks-content');
|
||||
var $icon = $(this).find('.es-icon');
|
||||
$blocksContent.stop(true, true);
|
||||
if ($blocksContent.is(':visible')) {
|
||||
$blocksContent.slideUp(200);
|
||||
var $icon = $(this).find('.material-icons');
|
||||
if ($blocksContent.hasClass('es-expanded')) {
|
||||
$blocksContent.removeClass('es-expanded');
|
||||
self.$wrapper.addClass('blocks-collapsed');
|
||||
$icon.text('expand_more');
|
||||
} else {
|
||||
$blocksContent.slideDown(200);
|
||||
$blocksContent.addClass('es-expanded');
|
||||
self.$wrapper.removeClass('blocks-collapsed');
|
||||
$icon.text('expand_less');
|
||||
}
|
||||
@@ -170,27 +168,23 @@
|
||||
});
|
||||
|
||||
// Toggle all groups (single button that switches between expand/collapse)
|
||||
console.log('[ES-DEBUG] Binding .btn-toggle-groups click on wrapper:', self.$wrapper.attr('id'), 'found buttons:', self.$wrapper.find('.btn-toggle-groups').length);
|
||||
this.$wrapper.on('click', '.btn-toggle-groups', function(e) {
|
||||
this.$wrapper.on('click', '.trait-header-actions .btn-toggle-groups', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var $btn = $(this);
|
||||
var currentState = $btn.attr('data-state') || 'collapsed';
|
||||
var trans = self.config.trans || {};
|
||||
console.log('[ES-DEBUG] .btn-toggle-groups CLICKED! currentState:', currentState, 'btn parent:', $btn.parent().attr('class'), 'groups found:', self.$wrapper.find('.selection-group').length);
|
||||
|
||||
if (currentState === 'collapsed') {
|
||||
self.$wrapper.find('.selection-group').removeClass('collapsed');
|
||||
$btn.attr('data-state', 'expanded');
|
||||
$btn.attr('title', trans.collapse_all || 'Collapse all groups');
|
||||
$btn.find('i').text('close_fullscreen');
|
||||
console.log('[ES-DEBUG] Expanded all groups');
|
||||
$btn.find('i').removeClass('icon-expand').addClass('icon-compress');
|
||||
} else {
|
||||
self.$wrapper.find('.selection-group').addClass('collapsed');
|
||||
$btn.attr('data-state', 'collapsed');
|
||||
$btn.attr('title', trans.expand_all || 'Expand all groups');
|
||||
$btn.find('i').text('open_in_full');
|
||||
console.log('[ES-DEBUG] Collapsed all groups');
|
||||
$btn.find('i').removeClass('icon-compress').addClass('icon-expand');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,15 +198,15 @@
|
||||
});
|
||||
|
||||
// Target switch change (PrestaShop native switch)
|
||||
this.$wrapper.on('change', '.target-switch-toggle', function(e) {
|
||||
this.$wrapper.on('change', '.es-switch-toggle', function(e) {
|
||||
e.stopPropagation();
|
||||
var value = $(this).val();
|
||||
if (value === '1') {
|
||||
self.clearAllConditions();
|
||||
self.$wrapper.find('.condition-trait-body').slideUp(200);
|
||||
self.$wrapper.find('.condition-trait-body').removeClass('es-expanded');
|
||||
self.$wrapper.addClass('collapsed');
|
||||
} else {
|
||||
self.$wrapper.find('.condition-trait-body').slideDown(200);
|
||||
self.$wrapper.find('.condition-trait-body').addClass('es-expanded');
|
||||
self.$wrapper.removeClass('collapsed');
|
||||
}
|
||||
});
|
||||
@@ -220,7 +214,7 @@
|
||||
// Add group
|
||||
this.$wrapper.on('click', '.btn-add-group', function(e) {
|
||||
e.preventDefault();
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
var blockType = $block.data('blockType');
|
||||
self.addGroup($block, blockType);
|
||||
});
|
||||
@@ -229,7 +223,7 @@
|
||||
this.$wrapper.on('click', '.btn-remove-group', function(e) {
|
||||
e.preventDefault();
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
self.removeGroup($group, $block);
|
||||
});
|
||||
|
||||
@@ -251,7 +245,7 @@
|
||||
this.$wrapper.on('click', '.btn-add-exclude', function(e) {
|
||||
e.preventDefault();
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
self.addFirstExcludeRow($group, $block);
|
||||
});
|
||||
|
||||
@@ -259,7 +253,7 @@
|
||||
this.$wrapper.on('click', '.btn-add-another-exclude', function(e) {
|
||||
e.preventDefault();
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
self.addExcludeRow($group, $block);
|
||||
});
|
||||
|
||||
@@ -268,16 +262,21 @@
|
||||
e.preventDefault();
|
||||
var $excludeRow = $(this).closest('.exclude-row');
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
self.removeExcludeRow($excludeRow, $group, $block);
|
||||
});
|
||||
|
||||
// Include method change
|
||||
this.$wrapper.on('change', '.include-method-select', function() {
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var newMethod = $(this).val();
|
||||
var prevMethod = $group.data('_currentIncludeMethod');
|
||||
if (prevMethod === newMethod) return;
|
||||
$group.data('_currentIncludeMethod', newMethod);
|
||||
|
||||
self.hideDropdown();
|
||||
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
var $row = $group.find('.group-include');
|
||||
var blockType = $block.data('blockType');
|
||||
var blockDef = self.config.blocks[blockType] || {};
|
||||
@@ -328,11 +327,16 @@
|
||||
|
||||
// Exclude method change (within an exclude row)
|
||||
this.$wrapper.on('change', '.exclude-method-select', function() {
|
||||
var $excludeRow = $(this).closest('.exclude-row');
|
||||
var newMethod = $(this).val();
|
||||
var prevMethod = $excludeRow.data('_currentExcludeMethod');
|
||||
if (prevMethod === newMethod) return;
|
||||
$excludeRow.data('_currentExcludeMethod', newMethod);
|
||||
|
||||
self.hideDropdown();
|
||||
|
||||
var $excludeRow = $(this).closest('.exclude-row');
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
var blockType = $block.data('blockType');
|
||||
var blockDef = self.config.blocks[blockType] || {};
|
||||
var methods = blockDef.selection_methods || {};
|
||||
@@ -510,8 +514,8 @@
|
||||
var currentPattern = $tag.data('pattern');
|
||||
|
||||
var $editInput = $('<input type="text" class="pattern-tag-edit">').val(currentPattern);
|
||||
var $saveBtn = $('<button type="button" class="btn-pattern-save" title="Save">' + this.esIcon('check') + '</button>');
|
||||
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel">' + this.esIcon('close') + '</button>');
|
||||
var $saveBtn = $('<button type="button" class="btn-pattern-save" title="Save"><i class="icon-check"></i></button>');
|
||||
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel"><i class="icon-times"></i></button>');
|
||||
var $editActions = $('<span class="pattern-edit-actions"></span>').append($saveBtn, $cancelBtn);
|
||||
|
||||
$tag.addClass('editing').find('.pattern-tag-text').hide();
|
||||
@@ -560,6 +564,50 @@
|
||||
$tag.removeClass('editing').find('.pattern-tag-text, .btn-remove-pattern').show();
|
||||
});
|
||||
|
||||
// Handle mpr-info-wrapper tooltip with fixed positioning
|
||||
this.$wrapper.on('mouseenter', '.mpr-info-wrapper[data-details]', function() {
|
||||
var $wrapper = $(this);
|
||||
if ($wrapper.data('tooltip-active')) return;
|
||||
|
||||
var content = $wrapper.attr('data-details');
|
||||
var tooltipClass = $wrapper.attr('data-tooltip-class') || '';
|
||||
var $tooltip = $('<div>', { class: 'mpr-tooltip mpr-tooltip-fixed ' + tooltipClass, html: content });
|
||||
|
||||
$('body').append($tooltip);
|
||||
$wrapper.data('tooltip-active', true);
|
||||
|
||||
var offset = $wrapper.offset();
|
||||
var triggerWidth = $wrapper.outerWidth();
|
||||
var tooltipWidth = $tooltip.outerWidth();
|
||||
var tooltipHeight = $tooltip.outerHeight();
|
||||
|
||||
var left = offset.left + (triggerWidth / 2) - (tooltipWidth / 2);
|
||||
var top = offset.top - tooltipHeight - 10;
|
||||
|
||||
if (left < 10) left = 10;
|
||||
if (left + tooltipWidth > $(window).width() - 10) {
|
||||
left = $(window).width() - tooltipWidth - 10;
|
||||
}
|
||||
|
||||
$tooltip.css({
|
||||
position: 'fixed',
|
||||
left: left + 'px',
|
||||
top: (top - $(window).scrollTop()) + 'px'
|
||||
});
|
||||
|
||||
$wrapper.data('tooltip-el', $tooltip);
|
||||
});
|
||||
|
||||
this.$wrapper.on('mouseleave', '.mpr-info-wrapper[data-details]', function() {
|
||||
var $wrapper = $(this);
|
||||
var $tooltip = $wrapper.data('tooltip-el');
|
||||
if ($tooltip) {
|
||||
$tooltip.remove();
|
||||
}
|
||||
$wrapper.data('tooltip-active', false);
|
||||
$wrapper.data('tooltip-el', null);
|
||||
});
|
||||
|
||||
// Handle numeric range input changes
|
||||
this.$wrapper.on('change', '.range-min-input, .range-max-input', function() {
|
||||
var $row = $(this).closest('.group-include, .exclude-row');
|
||||
@@ -655,7 +703,7 @@
|
||||
$chip.append($('<button>', {
|
||||
type: 'button',
|
||||
class: 'btn-remove-range',
|
||||
html: self.esIcon('close')
|
||||
html: '<i class="icon-times"></i>'
|
||||
}));
|
||||
|
||||
$chipsContainer.append($chip);
|
||||
@@ -768,9 +816,8 @@
|
||||
var $btn = $(this);
|
||||
var $modifiers = $btn.closest('.group-modifiers');
|
||||
var $content = $modifiers.find('.group-modifiers-content');
|
||||
$content.slideToggle(150, function() {
|
||||
$modifiers.toggleClass('expanded', $content.is(':visible'));
|
||||
});
|
||||
$content.toggleClass('es-expanded');
|
||||
$modifiers.toggleClass('expanded', $content.hasClass('es-expanded'));
|
||||
});
|
||||
|
||||
// Handle group-level modifier changes
|
||||
@@ -811,7 +858,11 @@
|
||||
$btn.attr('data-dir', newDir);
|
||||
|
||||
var $icon = $btn.find('i');
|
||||
$icon.replaceWith(this.esIcon('sort'));
|
||||
if (newDir === 'ASC') {
|
||||
$icon.removeClass('icon-sort-amount-desc').addClass('icon-sort-amount-asc');
|
||||
} else {
|
||||
$icon.removeClass('icon-sort-amount-asc').addClass('icon-sort-amount-desc');
|
||||
}
|
||||
|
||||
self.serializeAllBlocks();
|
||||
self.refreshGroupPreviewIfOpen($group);
|
||||
@@ -823,7 +874,7 @@
|
||||
e.stopPropagation();
|
||||
var $badge = $(this);
|
||||
var $group = $badge.closest('.selection-group');
|
||||
var $block = $badge.closest('.target-block');
|
||||
var $block = $badge.closest('.es-block');
|
||||
var blockType = $block.data('blockType');
|
||||
|
||||
if ($badge.hasClass('popover-open')) {
|
||||
@@ -838,7 +889,7 @@
|
||||
this.$wrapper.on('focus', '.entity-search-input', function() {
|
||||
var $picker = $(this).closest('.value-picker');
|
||||
var $group = $(this).closest('.selection-group');
|
||||
var $block = $(this).closest('.target-block');
|
||||
var $block = $(this).closest('.es-block');
|
||||
var blockType = $block.data('blockType');
|
||||
var groupIndex = parseInt($group.data('groupIndex'), 10);
|
||||
var section = $picker.hasClass('include-picker') ? 'include' : 'exclude';
|
||||
@@ -930,7 +981,7 @@
|
||||
if (query && self.activeGroup) {
|
||||
var $input = self.$wrapper.find('.entity-search-input:focus');
|
||||
if (!$input.length) {
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
$input = $group.find('.entity-search-input').first();
|
||||
}
|
||||
@@ -972,7 +1023,7 @@
|
||||
|
||||
if (!self.activeGroup) return;
|
||||
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
var $row;
|
||||
@@ -1040,8 +1091,6 @@
|
||||
var $row = $(this).closest('.group-include, .exclude-row');
|
||||
var id = $chip.data('id');
|
||||
|
||||
console.log('[EntitySelector] Chip remove clicked, id:', id);
|
||||
|
||||
// Also remove from pending selections if dropdown is open
|
||||
if (self.pendingSelections) {
|
||||
self.pendingSelections = self.pendingSelections.filter(function(s) {
|
||||
@@ -1050,7 +1099,6 @@
|
||||
}
|
||||
|
||||
self.removeSelection($picker, id);
|
||||
console.log('[EntitySelector] Calling serializeAllBlocks after chip remove');
|
||||
self.serializeAllBlocks($row);
|
||||
|
||||
if (self.$dropdown && self.$dropdown.hasClass('show')) {
|
||||
@@ -1140,7 +1188,7 @@
|
||||
}
|
||||
|
||||
// Handle list view
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
var $row;
|
||||
@@ -1219,7 +1267,7 @@
|
||||
}
|
||||
|
||||
// Handle list view
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
var $row;
|
||||
@@ -1306,7 +1354,7 @@
|
||||
var currentDir = $btn.data('dir');
|
||||
var newDir = currentDir === 'ASC' ? 'DESC' : 'ASC';
|
||||
$btn.data('dir', newDir);
|
||||
$btn.find('i').replaceWith(this.esIcon('sort_by_alpha'));
|
||||
$btn.find('i').attr('class', newDir === 'ASC' ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc');
|
||||
self.currentSort.dir = newDir;
|
||||
self.refreshSearch();
|
||||
});
|
||||
@@ -1320,7 +1368,8 @@
|
||||
$item.toggleClass('collapsed');
|
||||
var isCollapsed = $item.hasClass('collapsed');
|
||||
|
||||
$(this).find('i').text(isCollapsed ? 'arrow_right' : 'arrow_drop_down');
|
||||
$(this).find('i').toggleClass('icon-caret-down', !isCollapsed)
|
||||
.toggleClass('icon-caret-right', isCollapsed);
|
||||
|
||||
var descendants = self.findTreeDescendants($item, $allItems);
|
||||
for (var i = 0; i < descendants.length; i++) {
|
||||
@@ -1359,11 +1408,19 @@
|
||||
};
|
||||
|
||||
if (isSelected) {
|
||||
// Remove from pending
|
||||
// Remove from pending and remove chip
|
||||
self.pendingSelections = self.pendingSelections.filter(function(s) {
|
||||
return parseInt(s.id, 10) !== id;
|
||||
});
|
||||
$item.removeClass('selected');
|
||||
|
||||
// Remove chip immediately
|
||||
var $picker = self.$wrapper.find('.es-block.active .value-picker.include-picker, .es-block.active .value-picker.exclude-picker').first();
|
||||
if ($picker.length) {
|
||||
$picker.find('.entity-chip[data-id="' + id + '"]').remove();
|
||||
var $row = $picker.closest('.group-include, .exclude-row');
|
||||
self.serializeAllBlocks($row);
|
||||
}
|
||||
} else {
|
||||
// Validate selection before adding to pending
|
||||
var section = self.activeGroup.section;
|
||||
@@ -1373,7 +1430,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to pending
|
||||
// Add to pending and create chip immediately
|
||||
var exists = self.pendingSelections.some(function(s) {
|
||||
return parseInt(s.id, 10) === id;
|
||||
});
|
||||
@@ -1385,6 +1442,14 @@
|
||||
});
|
||||
}
|
||||
$item.addClass('selected');
|
||||
|
||||
// Immediately add chip (same as product list behavior)
|
||||
var $picker = self.$wrapper.find('.es-block.active .value-picker.include-picker, .es-block.active .value-picker.exclude-picker').first();
|
||||
if ($picker.length) {
|
||||
self.addSelection($picker, id, name, $item.data());
|
||||
var $row = $picker.closest('.group-include, .exclude-row');
|
||||
self.serializeAllBlocks($row);
|
||||
}
|
||||
}
|
||||
|
||||
updateCount();
|
||||
@@ -1416,7 +1481,7 @@
|
||||
|
||||
if (!self.activeGroup) return;
|
||||
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
var $row;
|
||||
@@ -1451,7 +1516,7 @@
|
||||
$child.removeClass('selected');
|
||||
}
|
||||
|
||||
$btn.find('i').replaceWith(self.esIcon('add_box'));
|
||||
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
|
||||
$btn.attr('title', trans.select_with_children || 'Select with all children');
|
||||
} else {
|
||||
var section = self.activeGroup.section;
|
||||
@@ -1489,7 +1554,7 @@
|
||||
self.showValidationError(skipMsg);
|
||||
}
|
||||
|
||||
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
|
||||
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
|
||||
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
|
||||
}
|
||||
|
||||
@@ -1510,7 +1575,7 @@
|
||||
this.$dropdown.on('click', '.category-tree .btn-expand-all', function(e) {
|
||||
e.preventDefault();
|
||||
self.$dropdown.find('.tree-item').removeClass('collapsed').show();
|
||||
self.$dropdown.find('.tree-toggle i').replaceWith(this.esIcon('arrow_drop_down'));
|
||||
self.$dropdown.find('.tree-toggle i').removeClass('icon-caret-right').addClass('icon-caret-down');
|
||||
});
|
||||
|
||||
// Tree view: Collapse all
|
||||
@@ -1530,7 +1595,7 @@
|
||||
if (level === minLevel) {
|
||||
if (hasChildren) {
|
||||
$item.addClass('collapsed');
|
||||
$item.find('.tree-toggle i').replaceWith(self.esIcon('arrow_right'));
|
||||
$item.find('.tree-toggle i').removeClass('icon-caret-down').addClass('icon-caret-right');
|
||||
}
|
||||
$item.show();
|
||||
} else {
|
||||
@@ -1888,8 +1953,8 @@
|
||||
// Click outside to close
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.value-picker').length &&
|
||||
!$(e.target).closest('.target-search-dropdown').length &&
|
||||
!$(e.target).closest('.target-preview-popover').length) {
|
||||
!$(e.target).closest('.es-search-dropdown').length &&
|
||||
!$(e.target).closest('.es-preview-popover').length) {
|
||||
self.hideDropdown();
|
||||
}
|
||||
});
|
||||
@@ -1928,6 +1993,104 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Tooltip hover events
|
||||
this.$wrapper.on('mouseenter', '.mpr-info-wrapper:not(.pinned)', function() {
|
||||
var $wrapper = $(this);
|
||||
var content = $wrapper.attr('data-tooltip');
|
||||
if (!content) return;
|
||||
|
||||
// Don't show hover tooltip if another is pinned
|
||||
if ($('.mpr-tooltip-fixed.pinned').length) return;
|
||||
|
||||
// Remove any existing non-pinned tooltip
|
||||
$('.mpr-tooltip-fixed:not(.pinned)').remove();
|
||||
|
||||
// Create tooltip
|
||||
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed' }).html(content);
|
||||
$('body').append($tooltip);
|
||||
|
||||
// Use getBoundingClientRect for viewport-relative positioning (fixed)
|
||||
var rect = $wrapper[0].getBoundingClientRect();
|
||||
var tooltipWidth = $tooltip.outerWidth();
|
||||
var tooltipHeight = $tooltip.outerHeight();
|
||||
|
||||
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
|
||||
var top = rect.top - tooltipHeight - 8;
|
||||
|
||||
// Keep tooltip within viewport
|
||||
if (left < 10) left = 10;
|
||||
if (left + tooltipWidth > window.innerWidth - 10) {
|
||||
left = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (top < 10) {
|
||||
top = rect.bottom + 8;
|
||||
}
|
||||
|
||||
$tooltip.css({ top: top, left: left });
|
||||
});
|
||||
|
||||
this.$wrapper.on('mouseleave', '.mpr-info-wrapper:not(.pinned)', function() {
|
||||
$('.mpr-tooltip-fixed:not(.pinned)').remove();
|
||||
});
|
||||
|
||||
// Click to pin tooltip
|
||||
this.$wrapper.on('click', '.mpr-info-wrapper', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var $wrapper = $(this);
|
||||
|
||||
// If already pinned, unpin and close
|
||||
if ($wrapper.hasClass('pinned')) {
|
||||
$wrapper.removeClass('pinned');
|
||||
$wrapper.find('.material-icons').text('info_outline');
|
||||
$('.mpr-tooltip-fixed.pinned').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any other pinned tooltips
|
||||
$('.mpr-info-wrapper.pinned').removeClass('pinned').find('.material-icons').text('info_outline');
|
||||
$('.mpr-tooltip-fixed').remove();
|
||||
|
||||
var content = $wrapper.attr('data-tooltip');
|
||||
if (!content) return;
|
||||
|
||||
// Pin this one
|
||||
$wrapper.addClass('pinned');
|
||||
$wrapper.find('.material-icons').text('close');
|
||||
|
||||
// Create pinned tooltip with close button
|
||||
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed pinned' });
|
||||
var $closeBtn = $('<button>', { class: 'mpr-tooltip-close', type: 'button' })
|
||||
.append($('<i>', { class: 'material-icons', text: 'close' }));
|
||||
$tooltip.append($closeBtn).append(content);
|
||||
$('body').append($tooltip);
|
||||
|
||||
// Close button click
|
||||
$closeBtn.on('click', function() {
|
||||
$wrapper.removeClass('pinned');
|
||||
$wrapper.find('.material-icons').text('info_outline');
|
||||
$tooltip.remove();
|
||||
});
|
||||
|
||||
// Position
|
||||
var rect = $wrapper[0].getBoundingClientRect();
|
||||
var tooltipWidth = $tooltip.outerWidth();
|
||||
var tooltipHeight = $tooltip.outerHeight();
|
||||
|
||||
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
|
||||
var top = rect.top - tooltipHeight - 8;
|
||||
|
||||
if (left < 10) left = 10;
|
||||
if (left + tooltipWidth > window.innerWidth - 10) {
|
||||
left = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (top < 10) {
|
||||
top = rect.bottom + 8;
|
||||
}
|
||||
|
||||
$tooltip.css({ top: top, left: left });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
|
||||
html += '<span class="toggle-name">' + group.name + '</span>';
|
||||
if (group.count !== undefined) {
|
||||
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + group.count + '</span>';
|
||||
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '"><i class="icon-eye"></i> ' + group.count + '</span>';
|
||||
}
|
||||
html += '</button>';
|
||||
$attrContainer.append(html);
|
||||
@@ -255,7 +255,7 @@
|
||||
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
|
||||
html += '<span class="toggle-name">' + group.name + '</span>';
|
||||
if (group.count !== undefined) {
|
||||
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + group.count + '</span>';
|
||||
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '"><i class="icon-eye"></i> ' + group.count + '</span>';
|
||||
}
|
||||
html += '</button>';
|
||||
$featContainer.append(html);
|
||||
@@ -310,7 +310,7 @@
|
||||
|
||||
// Add close button as sibling (outside filter-values-container, inside filter-row-values)
|
||||
$filterRowValues.find('.btn-close-values').remove();
|
||||
$filterRowValues.append('<button type="button" class="btn-close-values">' + this.esIcon('close') + '</button>');
|
||||
$filterRowValues.append('<button type="button" class="btn-close-values"><i class="icon-times"></i></button>');
|
||||
$filterRowValues.show();
|
||||
|
||||
// Scroll into view if needed
|
||||
|
||||
@@ -47,13 +47,13 @@
|
||||
|
||||
// Group header
|
||||
html += '<div class="group-header">';
|
||||
html += '<span class="group-collapse-toggle">' + this.esIcon('expand_less') + '</span>';
|
||||
html += '<span class="group-collapse-toggle"><i class="icon-chevron-up"></i></span>';
|
||||
html += '<span class="group-name-wrapper">';
|
||||
html += '<input type="text" class="group-name-input" value="" placeholder="' + defaultGroupName + '" title="' + (trans.click_to_name || 'Click to name this group') + '">';
|
||||
html += '<span class="group-count-badge" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
|
||||
html += '<span class="group-count-badge" style="display:none;"></span>';
|
||||
html += '</span>';
|
||||
html += '<button type="button" class="btn-remove-group" title="' + (trans.remove_group || 'Remove group') + '">';
|
||||
html += this.esIcon('delete');
|
||||
html += '<i class="icon-trash"></i>';
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -64,17 +64,21 @@
|
||||
html += '<div class="group-include">';
|
||||
html += '<div class="section-row">';
|
||||
html += '<div class="method-selector-wrapper">';
|
||||
html += '<select class="include-method-select">' + methodOptions + '</select>';
|
||||
html += '<span class="condition-match-count no-matches">' + this.esIcon('visibility') + ' <span class="preview-count">0</span></span>';
|
||||
html += '<span class="method-info-placeholder"></span>';
|
||||
html += '<select class="include-method-select">' + methodOptions + '</select>';
|
||||
html += '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
|
||||
html += '</div>';
|
||||
var noItemsText = trans.no_items_selected || 'No items selected - use search below';
|
||||
html += '<div class="value-picker include-picker" style="display:none;" data-search-entity="' + blockType + '">';
|
||||
html += '<div class="entity-chips include-chips" data-placeholder="' + noItemsText + '"></div>';
|
||||
html += '<div class="chips-wrapper">';
|
||||
html += '<div class="chips-toolbar" style="display:none;"></div>';
|
||||
html += '<div class="entity-chips include-chips" data-placeholder="' + noItemsText + '"><span class="chips-empty-state">' + noItemsText + '</span></div>';
|
||||
html += '<div class="chips-load-more" style="display:none;"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="entity-search-box">';
|
||||
html += this.esIcon('search', 'entity-search-icon');
|
||||
html += '<i class="icon-search entity-search-icon"></i>';
|
||||
html += '<input type="text" class="entity-search-input" placeholder="' + (trans.search_placeholder || 'Search by name, reference, ID...') + '" autocomplete="off">';
|
||||
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
|
||||
html += '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
|
||||
html += '</div>';
|
||||
html += '<input type="hidden" class="include-values-data" value="[]">';
|
||||
html += '</div>';
|
||||
@@ -84,7 +88,7 @@
|
||||
// Excludes section (collapsed by default)
|
||||
html += '<div class="group-excludes">';
|
||||
html += '<button type="button" class="btn-add-exclude">';
|
||||
html += this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions');
|
||||
html += '<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions');
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -106,11 +110,11 @@
|
||||
html += '<option value="random">' + (trans.sort_random || 'Random') + '</option>';
|
||||
html += '</select>';
|
||||
html += '<button type="button" class="btn-sort-dir" data-dir="DESC" title="' + (trans.sort_direction || 'Sort direction') + '">';
|
||||
html += this.esIcon('sort');
|
||||
html += '<i class="icon-sort-amount-desc"></i>';
|
||||
html += '</button>';
|
||||
html += '</span>';
|
||||
html += '<span class="group-preview-badge clickable" title="' + (trans.preview_results || 'Preview results') + '">';
|
||||
html += this.esIcon('visibility') + ' <span class="preview-count"></span>';
|
||||
html += '<i class="icon-eye"></i> <span class="preview-count"></span>';
|
||||
html += '</span>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -157,7 +161,7 @@
|
||||
var self = this;
|
||||
|
||||
// Remove all groups from all blocks
|
||||
this.$wrapper.find('.target-block').each(function() {
|
||||
this.$wrapper.find('.es-block').each(function() {
|
||||
var $block = $(this);
|
||||
var $container = $block.find('.groups-container');
|
||||
|
||||
@@ -186,41 +190,105 @@
|
||||
|
||||
switchToBlock: function(blockType) {
|
||||
// Update tabs
|
||||
this.$wrapper.find('.target-block-tab').removeClass('active');
|
||||
this.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]').addClass('active');
|
||||
this.$wrapper.find('.es-block-tab').removeClass('active');
|
||||
this.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]').addClass('active');
|
||||
|
||||
// Update blocks
|
||||
this.$wrapper.find('.target-block').removeClass('active').hide();
|
||||
this.$wrapper.find('.target-block[data-block-type="' + blockType + '"]').addClass('active').show();
|
||||
this.$wrapper.find('.es-block').removeClass('active').hide();
|
||||
this.$wrapper.find('.es-block[data-block-type="' + blockType + '"]').addClass('active').show();
|
||||
|
||||
// Close dropdown if open
|
||||
this.hideDropdown();
|
||||
},
|
||||
|
||||
/**
|
||||
* Single-mode badge update: read chip counts from DOM, set directly.
|
||||
* No AJAX, no spinners, no updateBlockStatus.
|
||||
*/
|
||||
_updateSingleModeBadges: function() {
|
||||
var self = this;
|
||||
this.$wrapper.find('.es-block-tab').each(function() {
|
||||
var $tab = $(this);
|
||||
var blockType = $tab.data('blockType');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
|
||||
var $badge = $tab.find('.tab-badge');
|
||||
|
||||
// Custom blocks: check input value
|
||||
if ($block.hasClass('custom-block')) {
|
||||
var hasVal = false;
|
||||
$block.find('.custom-block-content').find('input, textarea, select').each(function() {
|
||||
if ($(this).val() && $(this).val().trim() !== '') { hasVal = true; return false; }
|
||||
});
|
||||
if (hasVal) {
|
||||
if (!$badge.length) { $tab.append('<span class="tab-badge"><i class="icon-check"></i></span>'); }
|
||||
else { $badge.html('<i class="icon-check"></i>'); }
|
||||
$tab.addClass('has-data');
|
||||
} else {
|
||||
$badge.remove();
|
||||
$tab.removeClass('has-data');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Entity blocks: count chips
|
||||
var chipCount = $block.find('.entity-chip:not(.entity-chip-loading)').length;
|
||||
if (chipCount > 0) {
|
||||
if (!$badge.length) { $tab.append('<span class="tab-badge"><i class="icon-eye"></i> ' + chipCount + '</span>'); }
|
||||
else { $badge.html('<i class="icon-eye"></i> ' + chipCount); }
|
||||
$tab.addClass('has-data');
|
||||
} else {
|
||||
$badge.remove();
|
||||
$tab.removeClass('has-data');
|
||||
}
|
||||
});
|
||||
|
||||
// Also update inline condition count (eye icon next to "Specific products")
|
||||
this.$wrapper.find('.es-block.active').each(function() {
|
||||
var $block = $(this);
|
||||
var chipCount = $block.find('.entity-chip:not(.entity-chip-loading)').length;
|
||||
var $countEl = $block.find('.condition-match-count').first();
|
||||
self._setBadgeCount($countEl, chipCount);
|
||||
var $groupBadge = $block.find('.group-count-badge').first();
|
||||
if ($groupBadge.length) {
|
||||
self._setBadgeCount($groupBadge, chipCount);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Set badge to correct visual state based on count.
|
||||
* ONE function for ALL badge types — no more class juggling in 20 places.
|
||||
*/
|
||||
_setBadgeCount: function($el, count) {
|
||||
if (!$el || !$el.length) return;
|
||||
var html = '<i class="icon-eye"></i> ' + count;
|
||||
if ($el.html() !== html) $el.html(html);
|
||||
$el.removeClass('loading loading-count no-matches clickable');
|
||||
$el.addClass(count > 0 ? 'clickable' : 'no-matches');
|
||||
$el.show();
|
||||
},
|
||||
|
||||
updateTabBadges: function() {
|
||||
var self = this;
|
||||
|
||||
// Collect all block types with data and set loading state
|
||||
var blockTypesWithData = [];
|
||||
this.$wrapper.find('.target-block-tab').each(function() {
|
||||
this.$wrapper.find('.es-block-tab').each(function() {
|
||||
var $tab = $(this);
|
||||
var blockType = $tab.data('blockType');
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + blockType + '"]');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
|
||||
var groupCount = $block.find('.selection-group').length;
|
||||
|
||||
// Update or add badge
|
||||
var $badge = $tab.find('.tab-badge');
|
||||
|
||||
if (groupCount > 0) {
|
||||
// Show loading state first
|
||||
if ($badge.length) {
|
||||
$badge.addClass('loading').html(self.esIcon('progress_activity', 'es-spin'));
|
||||
} else {
|
||||
$tab.append('<span class="tab-badge loading">' + self.esIcon('progress_activity', 'es-spin') + '</span>');
|
||||
// Has data — show spinner ONLY if no badge exists yet.
|
||||
// If badge already has a count, keep it visible while AJAX refreshes.
|
||||
if (!$badge.length) {
|
||||
$tab.append('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
|
||||
}
|
||||
$tab.addClass('has-data');
|
||||
blockTypesWithData.push(blockType);
|
||||
} else if ($block.hasClass('custom-block')) {
|
||||
// Custom blocks: check if any input/textarea/select has a non-empty value
|
||||
var hasCustomValue = false;
|
||||
$block.find('.custom-block-content').find('input, textarea, select').each(function() {
|
||||
if ($(this).val() && $(this).val().trim() !== '') {
|
||||
@@ -229,11 +297,8 @@
|
||||
}
|
||||
});
|
||||
if (hasCustomValue) {
|
||||
if ($badge.length) {
|
||||
$badge.removeClass('loading').html(self.esIcon('check'));
|
||||
} else {
|
||||
$tab.append('<span class="tab-badge">' + self.esIcon('check') + '</span>');
|
||||
}
|
||||
if (!$badge.length) { $tab.append('<span class="tab-badge"><i class="icon-check"></i></span>'); }
|
||||
else { $badge.removeClass('loading').html('<i class="icon-check"></i>'); }
|
||||
$tab.addClass('has-data');
|
||||
} else {
|
||||
$badge.remove();
|
||||
@@ -245,10 +310,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Update target switch state based on whether any data exists
|
||||
this.updateTargetSwitchState();
|
||||
|
||||
// Fetch all counts in a single bulk request
|
||||
if (blockTypesWithData.length > 0) {
|
||||
this.fetchAllCounts(blockTypesWithData);
|
||||
}
|
||||
@@ -262,7 +325,7 @@
|
||||
|
||||
// Check if any block has data
|
||||
var hasData = false;
|
||||
this.$wrapper.find('.target-block').each(function() {
|
||||
this.$wrapper.find('.es-block').each(function() {
|
||||
if ($(this).find('.selection-group').length > 0) {
|
||||
hasData = true;
|
||||
return false; // break
|
||||
@@ -284,7 +347,6 @@
|
||||
fetchAllCounts: function(blockTypes) {
|
||||
var self = this;
|
||||
|
||||
// Read saved data from hidden input
|
||||
var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
|
||||
var savedData = {};
|
||||
try {
|
||||
@@ -302,17 +364,25 @@
|
||||
}
|
||||
});
|
||||
|
||||
// If no valid conditions, remove loading spinners
|
||||
// Blocks with groups but no matchable conditions: show 0
|
||||
blockTypes.forEach(function(blockType) {
|
||||
if (!(blockType in conditions)) {
|
||||
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
|
||||
var $badge = $tab.find('.tab-badge');
|
||||
if (!$badge.length) { $badge = $('<span class="tab-badge"></span>'); $tab.append($badge); }
|
||||
self._setBadgeCount($badge, 0);
|
||||
$tab.addClass('has-data');
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
|
||||
self._setBadgeCount($block.find('.condition-match-count').first(), 0);
|
||||
self._setBadgeCount($block.find('.group-count-badge').first(), 0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (Object.keys(conditions).length === 0) {
|
||||
blockTypes.forEach(function(blockType) {
|
||||
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
|
||||
$tab.find('.tab-badge').remove();
|
||||
$tab.removeClass('has-data');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Single bulk AJAX request for all counts
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
@@ -325,23 +395,26 @@
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.counts) {
|
||||
// Update each tab with its count
|
||||
Object.keys(response.counts).forEach(function(blockType) {
|
||||
var count = response.counts[blockType];
|
||||
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
|
||||
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
|
||||
var $badge = $tab.find('.tab-badge');
|
||||
|
||||
if ($badge.length) {
|
||||
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + count);
|
||||
// Store preview data for later popover use
|
||||
$tab.data('previewData', { count: count, success: true });
|
||||
}
|
||||
self._setBadgeCount($badge, count);
|
||||
$tab.data('previewData', { count: count, success: true });
|
||||
});
|
||||
|
||||
// Handle any block types not in response (set count to 0 or remove badge)
|
||||
// Also update inline + group badges
|
||||
Object.keys(response.counts).forEach(function(blockType) {
|
||||
var count = response.counts[blockType];
|
||||
var $block = self.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
|
||||
self._setBadgeCount($block.find('.condition-match-count').first(), count);
|
||||
self._setBadgeCount($block.find('.group-count-badge').first(), count);
|
||||
});
|
||||
|
||||
|
||||
blockTypes.forEach(function(blockType) {
|
||||
if (!(blockType in response.counts)) {
|
||||
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
|
||||
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
|
||||
$tab.find('.tab-badge').remove();
|
||||
$tab.removeClass('has-data');
|
||||
}
|
||||
@@ -352,7 +425,7 @@
|
||||
console.error('[EntitySelector] Bulk preview failed:', response.error || 'Unknown error');
|
||||
// Remove loading spinners on error
|
||||
blockTypes.forEach(function(blockType) {
|
||||
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
|
||||
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
|
||||
$tab.find('.tab-badge').remove();
|
||||
});
|
||||
}
|
||||
@@ -361,7 +434,7 @@
|
||||
console.error('[EntitySelector] Bulk AJAX error:', status, error);
|
||||
// Remove loading spinners on error
|
||||
blockTypes.forEach(function(blockType) {
|
||||
var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
|
||||
var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
|
||||
$tab.find('.tab-badge').remove();
|
||||
});
|
||||
}
|
||||
@@ -397,10 +470,10 @@
|
||||
// Show loading state
|
||||
var $badge = $tab.find('.tab-badge');
|
||||
if (!$badge.length) {
|
||||
$badge = $('<span class="tab-badge loading">' + this.esIcon('progress_activity', 'es-spin') + '</span>');
|
||||
$badge = $('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
|
||||
$tab.append($badge);
|
||||
} else {
|
||||
$badge.addClass('loading').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$badge.addClass('loading').html('<i class="icon-spinner icon-spin"></i>');
|
||||
}
|
||||
$tab.addClass('has-data');
|
||||
|
||||
@@ -421,7 +494,7 @@
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
var $badge = $tab.find('.tab-badge');
|
||||
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + response.count);
|
||||
$badge.removeClass('loading').html('<i class="icon-eye"></i> ' + response.count);
|
||||
|
||||
// Store preview data for popover
|
||||
$tab.data('previewData', response);
|
||||
@@ -446,7 +519,7 @@
|
||||
var total = 0;
|
||||
|
||||
// Sum up all tab badge counts
|
||||
this.$wrapper.find('.target-block-tab .tab-badge').each(function() {
|
||||
this.$wrapper.find('.es-block-tab .tab-badge').each(function() {
|
||||
var $badge = $(this);
|
||||
if (!$badge.hasClass('loading')) {
|
||||
var count = parseInt($badge.text(), 10);
|
||||
@@ -473,7 +546,7 @@
|
||||
if (!$toggle.length) return;
|
||||
|
||||
var $checkbox = $toggle.find('.show-all-checkbox');
|
||||
var hasData = this.$wrapper.find('.target-block-tab.has-data').length > 0;
|
||||
var hasData = this.$wrapper.find('.es-block-tab.has-data').length > 0;
|
||||
|
||||
// If there's data, uncheck (not showing to all), otherwise check
|
||||
$checkbox.prop('checked', !hasData);
|
||||
@@ -568,16 +641,11 @@
|
||||
var self = this;
|
||||
var data = {};
|
||||
|
||||
console.log('[EntitySelector] serializeAllBlocks called');
|
||||
|
||||
this.$wrapper.find('.target-block').each(function() {
|
||||
this.$wrapper.find('.es-block').each(function() {
|
||||
var $block = $(this);
|
||||
var blockType = $block.data('blockType');
|
||||
var groups = self.getBlockGroups($block);
|
||||
|
||||
console.log('[EntitySelector] Block:', blockType, 'Groups:', groups.length);
|
||||
|
||||
// Groups now contain their own modifiers, no block-level modifiers
|
||||
if (groups.length > 0) {
|
||||
data[blockType] = { groups: groups };
|
||||
}
|
||||
@@ -585,37 +653,35 @@
|
||||
self.updateBlockStatus($block);
|
||||
});
|
||||
|
||||
// Update hidden input first
|
||||
var $input = this.$wrapper.find('input[name="' + this.config.name + '"]');
|
||||
var jsonData = JSON.stringify(data);
|
||||
|
||||
console.log('[EntitySelector] Hidden input name:', this.config.name);
|
||||
console.log('[EntitySelector] Hidden input found:', $input.length);
|
||||
console.log('[EntitySelector] Serialized data:', jsonData.substring(0, 500));
|
||||
// Skip if data hasn't changed since last serialization
|
||||
if (this._lastSerializedData === jsonData) return;
|
||||
this._lastSerializedData = jsonData;
|
||||
|
||||
$input.val(jsonData);
|
||||
|
||||
// Then update tab badges (reads from hidden input)
|
||||
this.updateTabBadges();
|
||||
|
||||
// Debounced update of condition count - only for changed row if specified
|
||||
if (this.countUpdateTimeout) {
|
||||
clearTimeout(this.countUpdateTimeout);
|
||||
// Single mode: update badges from chip count directly, no AJAX
|
||||
if (this.config.mode === 'single') {
|
||||
this._updateSingleModeBadges();
|
||||
return;
|
||||
}
|
||||
this.countUpdateTimeout = setTimeout(function() {
|
||||
|
||||
// Multi mode: debounce ALL visual updates so rapid serialize calls collapse
|
||||
if (this._visualUpdateTimer) clearTimeout(this._visualUpdateTimer);
|
||||
this._visualUpdateTimer = setTimeout(function() {
|
||||
self.updateTabBadges();
|
||||
if ($changedRow && $changedRow.length) {
|
||||
// Update the specific row that changed
|
||||
self.updateConditionCount($changedRow);
|
||||
// Also update the group total count (include - excludes)
|
||||
var $group = $changedRow.closest('.selection-group');
|
||||
if ($group.length) {
|
||||
self.updateGroupTotalCount($group);
|
||||
}
|
||||
} else {
|
||||
// Fallback: update all counts (initial load, structure changes)
|
||||
self.updateAllConditionCounts();
|
||||
}
|
||||
}, 500);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
getBlockGroups: function($block) {
|
||||
@@ -852,9 +918,9 @@
|
||||
var conditionIndex = 0;
|
||||
|
||||
// Collect all conditions from all active groups
|
||||
this.$wrapper.find('.target-block.active .selection-group').each(function() {
|
||||
this.$wrapper.find('.es-block.active .selection-group').each(function() {
|
||||
var $group = $(this);
|
||||
var $block = $group.closest('.target-block');
|
||||
var $block = $group.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
|
||||
// Process include row
|
||||
@@ -905,10 +971,9 @@
|
||||
$countEl.removeClass('no-matches clickable');
|
||||
if (count === 0) {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('no-matches').show();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
} else {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('clickable').show();
|
||||
self._setBadgeCount($countEl, count);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -920,7 +985,7 @@
|
||||
Object.keys(conditionElements).forEach(function(id) {
|
||||
var $countEl = conditionElements[id];
|
||||
if ($countEl && $countEl.length) {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -931,9 +996,7 @@
|
||||
* Extract condition data from a row for bulk counting
|
||||
*/
|
||||
getConditionData: function($row, blockType) {
|
||||
console.log('[getConditionData] Called with blockType:', blockType);
|
||||
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
|
||||
console.log('[getConditionData] $countEl found:', $countEl.length);
|
||||
if (!$countEl.length) return null;
|
||||
|
||||
var isExclude = $row.hasClass('exclude-row');
|
||||
@@ -942,23 +1005,18 @@
|
||||
: $row.find('.include-method-select');
|
||||
|
||||
var method = $methodSelect.val();
|
||||
console.log('[getConditionData] method:', method);
|
||||
if (!method) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return null;
|
||||
}
|
||||
|
||||
var $picker = isExclude
|
||||
? $row.find('.exclude-picker')
|
||||
: $row.find('.include-picker');
|
||||
console.log('[getConditionData] $picker found:', $picker.length, 'data-value-type attr:', $picker.attr('data-value-type'));
|
||||
|
||||
var valueType = $picker.data('valueType') || $picker.attr('data-value-type') || 'none';
|
||||
console.log('[getConditionData] valueType:', valueType);
|
||||
|
||||
// Special case: "All countries" method - needs separate handling for holidays
|
||||
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
|
||||
console.log('[getConditionData] All countries detected - triggering updateConditionCount');
|
||||
// Trigger separate update for this special case (uses nested AJAX)
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
@@ -970,7 +1028,6 @@
|
||||
// Special case: Specific countries with entity_search - needs holiday counting, not entity counting
|
||||
var searchEntity = $picker.attr('data-search-entity') || '';
|
||||
if (blockType === 'countries' && valueType === 'entity_search' && searchEntity === 'countries') {
|
||||
console.log('[getConditionData] Specific countries detected - triggering updateConditionCount for holiday counting');
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.updateConditionCount($row, blockType);
|
||||
@@ -978,9 +1035,14 @@
|
||||
return null; // Skip bulk processing, handled separately
|
||||
}
|
||||
|
||||
// Hide badge for other "all" type methods (valueType === 'none') since they don't filter
|
||||
// "All" type methods (valueType === 'none') — store conditionData for preview, count comes from fetchAllCounts
|
||||
if (valueType === 'none') {
|
||||
$countEl.hide();
|
||||
$countEl.data('conditionData', {
|
||||
method: method,
|
||||
values: [],
|
||||
blockType: blockType,
|
||||
isExclude: isExclude
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -994,13 +1056,12 @@
|
||||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
|
||||
));
|
||||
if (valueType !== 'boolean' && hasNoValues) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show loading spinner
|
||||
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$countEl.removeClass('clickable no-matches').show();
|
||||
// Show loading state — keep existing count visible, just dim it
|
||||
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
|
||||
|
||||
// Store condition data on badge for popover
|
||||
$countEl.data('conditionData', {
|
||||
@@ -1022,7 +1083,7 @@
|
||||
|
||||
updateGroupCounts: function($group) {
|
||||
var self = this;
|
||||
var $block = $group.closest('.target-block');
|
||||
var $block = $group.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
|
||||
// Update include count
|
||||
@@ -1048,7 +1109,6 @@
|
||||
|
||||
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
|
||||
if (!$countEl.length) {
|
||||
console.log('[updateConditionCount] No $countEl found');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1058,10 +1118,8 @@
|
||||
: $row.find('.include-method-select');
|
||||
|
||||
var method = $methodSelect.val();
|
||||
console.log('[updateConditionCount] method:', method, 'isExclude:', isExclude);
|
||||
if (!method) {
|
||||
console.log('[updateConditionCount] No method, hiding badge');
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1074,17 +1132,13 @@
|
||||
|
||||
// Get the block type to check if this is a countries block
|
||||
if (!blockType) {
|
||||
var $block = $row.closest('.target-block');
|
||||
var $block = $row.closest('.es-block');
|
||||
blockType = $block.data('blockType') || 'products';
|
||||
}
|
||||
|
||||
console.log('[updateConditionCount] valueType:', valueType, 'searchEntity:', searchEntity, 'blockType:', blockType, 'method:', method);
|
||||
|
||||
// Special case: "All countries" method - fetch holidays for all countries
|
||||
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
|
||||
console.log('[updateConditionCount] All countries method - fetching all country holidays');
|
||||
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$countEl.removeClass('clickable no-matches country-holidays').show();
|
||||
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
|
||||
|
||||
// First fetch all active country IDs, then get holidays
|
||||
$.ajax({
|
||||
@@ -1103,7 +1157,6 @@
|
||||
var items = response.results || response.items || [];
|
||||
if (response && response.success && items.length > 0) {
|
||||
var allCountryIds = items.map(function(item) { return item.id; });
|
||||
console.log('[updateConditionCount] Found', allCountryIds.length, 'countries, fetching holidays');
|
||||
|
||||
// Store condition data for click handler
|
||||
$countEl.data('conditionData', {
|
||||
@@ -1129,42 +1182,44 @@
|
||||
count_only: 1
|
||||
},
|
||||
success: function(holidayResponse) {
|
||||
console.log('[updateConditionCount] All countries holiday response:', holidayResponse);
|
||||
if (holidayResponse && holidayResponse.success) {
|
||||
var count = holidayResponse.total_count || 0;
|
||||
$countEl.removeClass('no-matches clickable');
|
||||
$countEl.addClass('country-holidays');
|
||||
if (count === 0) {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('no-matches').show();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
} else {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('clickable').show();
|
||||
self._setBadgeCount($countEl, count);
|
||||
}
|
||||
$countEl.data('countriesInfo', holidayResponse.countries || []);
|
||||
} else {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide badge for other "all" type methods (valueType === 'none') since they don't filter
|
||||
// "All" type methods (valueType === 'none') — store conditionData for preview
|
||||
if (valueType === 'none') {
|
||||
console.log('[updateConditionCount] valueType is none, hiding badge');
|
||||
$countEl.hide();
|
||||
$countEl.data('conditionData', {
|
||||
method: method,
|
||||
values: [],
|
||||
blockType: blockType,
|
||||
isExclude: isExclude
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1177,25 +1232,22 @@
|
||||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
|
||||
));
|
||||
if (valueType !== 'boolean' && hasNoValues) {
|
||||
$countEl.hide();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!blockType) {
|
||||
var $block = $row.closest('.target-block');
|
||||
var $block = $row.closest('.es-block');
|
||||
blockType = $block.data('blockType') || 'products';
|
||||
}
|
||||
|
||||
// Check if this is a country selection - show holiday count instead
|
||||
var isCountrySelection = (searchEntity === 'countries' && valueType === 'entity_search');
|
||||
console.log('[updateConditionCount] isCountrySelection:', isCountrySelection, 'values:', values);
|
||||
|
||||
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
|
||||
$countEl.removeClass('clickable no-matches country-holidays').show();
|
||||
if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
|
||||
|
||||
// For countries, fetch holiday count
|
||||
if (isCountrySelection && Array.isArray(values) && values.length > 0) {
|
||||
console.log('[updateConditionCount] Fetching holiday count for countries:', values);
|
||||
$countEl.data('conditionData', {
|
||||
method: method,
|
||||
values: values,
|
||||
@@ -1217,28 +1269,24 @@
|
||||
count_only: 1
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('[updateConditionCount] Holiday response:', response);
|
||||
if (response && response.success) {
|
||||
var count = response.total_count || 0;
|
||||
console.log('[updateConditionCount] Holiday count:', count);
|
||||
$countEl.removeClass('no-matches clickable');
|
||||
$countEl.addClass('country-holidays');
|
||||
if (count === 0) {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('no-matches').show();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
} else {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('clickable').show();
|
||||
self._setBadgeCount($countEl, count);
|
||||
}
|
||||
// Store countries info for popover
|
||||
$countEl.data('countriesInfo', response.countries || []);
|
||||
} else {
|
||||
console.log('[updateConditionCount] Holiday response failed:', response);
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -1270,24 +1318,23 @@
|
||||
$countEl.removeClass('no-matches clickable');
|
||||
if (count === 0) {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('no-matches').show();
|
||||
self._setBadgeCount($countEl, 0);
|
||||
} else {
|
||||
$countEl.find('.preview-count').text(count);
|
||||
$countEl.addClass('clickable').show();
|
||||
self._setBadgeCount($countEl, count);
|
||||
}
|
||||
} else {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$countEl.hide().removeClass('clickable');
|
||||
self._setBadgeCount($countEl, 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateGroupTotalCount: function($group) {
|
||||
var self = this;
|
||||
var $block = $group.closest('.target-block');
|
||||
var $block = $group.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
var $badge = $group.find('.group-header .group-count-badge');
|
||||
var $limitInput = $group.find('.group-modifier-limit');
|
||||
@@ -1303,7 +1350,7 @@
|
||||
}
|
||||
|
||||
// Show loading
|
||||
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
|
||||
if (!$badge.hasClass('clickable')) { $badge.addClass('loading-count'); } $badge.show();
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -1321,13 +1368,14 @@
|
||||
var finalCount = response.final_count || 0;
|
||||
var excludeCount = response.exclude_count || 0;
|
||||
|
||||
// Update badge with eye icon and count
|
||||
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
|
||||
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
|
||||
if (excludeCount > 0) {
|
||||
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
|
||||
}
|
||||
$badge.html(badgeHtml);
|
||||
$badge.addClass('clickable').show();
|
||||
if ($badge.html() !== badgeHtml) {
|
||||
$badge.html(badgeHtml);
|
||||
}
|
||||
self._setBadgeCount($badge, finalCount);
|
||||
|
||||
// Store group data on badge for preview popover
|
||||
$badge.data('groupData', groupData);
|
||||
@@ -1345,7 +1393,7 @@
|
||||
$previewBadge.text(displayCount);
|
||||
}
|
||||
} else {
|
||||
$badge.hide().removeClass('clickable');
|
||||
self._setBadgeCount($badge, 0);
|
||||
$limitInput.attr('placeholder', '–');
|
||||
}
|
||||
},
|
||||
@@ -1363,7 +1411,7 @@
|
||||
|
||||
// Build the full excludes structure with first row
|
||||
var html = '<div class="except-separator">';
|
||||
html += '<span class="except-label">' + this.esIcon('block') + ' ' + (trans.except || 'EXCEPT') + '</span>';
|
||||
html += '<span class="except-label"><i class="icon-ban"></i> ' + (trans.except || 'EXCEPT') + '</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="exclude-rows-container">';
|
||||
@@ -1371,7 +1419,7 @@
|
||||
html += '</div>';
|
||||
|
||||
html += '<button type="button" class="btn-add-another-exclude">';
|
||||
html += this.esIcon('add') + ' ' + (trans.add_another_exception || 'Add another exception');
|
||||
html += '<i class="icon-plus"></i> ' + (trans.add_another_exception || 'Add another exception');
|
||||
html += '</button>';
|
||||
|
||||
$excludesDiv.addClass('has-excludes').html(html);
|
||||
@@ -1442,11 +1490,11 @@
|
||||
html += '<div class="exclude-header-row">';
|
||||
html += '<div class="method-selector-wrapper">';
|
||||
html += '<select class="exclude-method-select">' + excludeMethodOptions + '</select>';
|
||||
html += '<span class="condition-match-count no-matches">' + this.esIcon('visibility') + ' <span class="preview-count">0</span></span>';
|
||||
html += '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
|
||||
html += '<span class="method-info-placeholder"></span>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="btn-remove-exclude-row" title="' + (trans.remove_this_exception || 'Remove this exception') + '">';
|
||||
html += this.esIcon('delete');
|
||||
html += '<i class="icon-trash"></i>';
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -1472,7 +1520,7 @@
|
||||
var $excludesDiv = $group.find('.group-excludes');
|
||||
$excludesDiv.removeClass('has-excludes').html(
|
||||
'<button type="button" class="btn-add-exclude">' +
|
||||
this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions') +
|
||||
'<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions') +
|
||||
'</button>'
|
||||
);
|
||||
// Unlock the method selector since no excludes exist
|
||||
@@ -1576,11 +1624,15 @@
|
||||
switch (valueType) {
|
||||
case 'entity_search':
|
||||
var noItemsText = trans.no_items_selected || 'No items selected - use search below';
|
||||
html += '<div class="entity-chips ' + chipsClass + '" data-placeholder="' + this.escapeAttr(noItemsText) + '"></div>';
|
||||
html += '<div class="chips-wrapper">';
|
||||
html += '<div class="chips-toolbar" style="display:none;"></div>';
|
||||
html += '<div class="entity-chips ' + chipsClass + '" data-placeholder="' + this.escapeAttr(noItemsText) + '"><span class="chips-empty-state">' + this.escapeHtml(noItemsText) + '</span></div>';
|
||||
html += '<div class="chips-load-more" style="display:none;"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="entity-search-box">';
|
||||
html += this.esIcon('search', 'entity-search-icon');
|
||||
html += '<i class="icon-search entity-search-icon"></i>';
|
||||
html += '<input type="text" class="entity-search-input" placeholder="' + this.escapeAttr(trans.search_placeholder || 'Search by name, reference, ID...') + '" autocomplete="off">';
|
||||
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
|
||||
html += '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
|
||||
html += '</div>';
|
||||
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
|
||||
break;
|
||||
@@ -1607,12 +1659,10 @@
|
||||
html += '<div class="pattern-tag draft-tag" data-case-sensitive="0">';
|
||||
html += '<button type="button" class="btn-toggle-case" title="' + this.escapeAttr(trans.case_insensitive || 'Case insensitive - click to toggle') + '"><span class="case-icon">aa</span></button>';
|
||||
html += '<input type="text" class="pattern-input" value="" placeholder="' + this.escapeAttr(trans.enter_pattern || 'e.g. *cotton*') + '">';
|
||||
html += '<span class="pattern-match-count" title="' + this.escapeAttr(trans.click_to_preview || 'Click to preview matches') + '">' + this.esIcon('visibility') + ' <span class="count-value"></span></span>';
|
||||
html += '<button type="button" class="btn-add-pattern" title="' + this.escapeAttr(trans.add_pattern || 'Add pattern (Enter)') + '">' + this.esIcon('add') + '</button>';
|
||||
html += '<span class="pattern-match-count" title="' + this.escapeAttr(trans.click_to_preview || 'Click to preview matches') + '"><i class="icon-eye"></i> <span class="count-value"></span></span>';
|
||||
html += '<button type="button" class="btn-add-pattern" title="' + this.escapeAttr(trans.add_pattern || 'Add pattern (Enter)') + '"><i class="icon-plus"></i></button>';
|
||||
html += '</div>';
|
||||
html += '<span class="mpr-info-wrapper" data-details="' + this.escapeAttr(tooltipContent) + '">';
|
||||
html += this.esIcon('info');
|
||||
html += '</span>';
|
||||
html += this._buildInfoTooltip(tooltipContent, 'details');
|
||||
html += '</div>';
|
||||
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
|
||||
break;
|
||||
@@ -1633,7 +1683,7 @@
|
||||
html += '<input type="number" class="range-min-input" value="" placeholder="' + this.escapeAttr(trans.min || 'Min') + '" step="0.01">';
|
||||
html += '<span class="range-separator">-</span>';
|
||||
html += '<input type="number" class="range-max-input" value="" placeholder="' + this.escapeAttr(trans.max || 'Max') + '" step="0.01">';
|
||||
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '">' + this.esIcon('add') + '</button>';
|
||||
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '"><i class="icon-plus"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
|
||||
@@ -1701,7 +1751,7 @@
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="combination-groups-container">';
|
||||
html += '<span class="combination-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
|
||||
html += '<span class="combination-loading"><i class="icon-spinner icon-spin"></i> ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
// Store mode along with attributes: { mode: 'products'|'combinations', attributes: { groupId: [valueIds] } }
|
||||
@@ -1747,14 +1797,28 @@
|
||||
}
|
||||
},
|
||||
|
||||
getSortIconName: function(sortBy, sortDir) {
|
||||
getSortIconClass: function(sortBy, sortDir) {
|
||||
var isAsc = (sortDir === 'ASC');
|
||||
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
return 'sort_by_alpha';
|
||||
return isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
|
||||
case 'price':
|
||||
case 'quantity':
|
||||
case 'product_count':
|
||||
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
|
||||
case 'date_add':
|
||||
case 'newest_products':
|
||||
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
|
||||
case 'sales':
|
||||
case 'total_sales':
|
||||
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
|
||||
case 'position':
|
||||
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
|
||||
case 'random':
|
||||
return 'shuffle';
|
||||
return 'icon-random';
|
||||
default:
|
||||
return 'sort';
|
||||
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1799,7 +1863,7 @@
|
||||
$btn.attr('data-sort', newSort);
|
||||
$btn.attr('data-dir', newDir);
|
||||
$btn.attr('title', newLabel + ' ' + (newDir === 'DESC' ? '↓' : '↑'));
|
||||
$btn.find('i').replaceWith(this.esIcon(this.getSortIconName(newSort, newDir)));
|
||||
$btn.find('i').attr('class', this.getSortIconClass(newSort, newDir));
|
||||
},
|
||||
|
||||
// Validation
|
||||
@@ -1811,7 +1875,7 @@
|
||||
|
||||
// Check if any block has data (groups with selections)
|
||||
var hasData = false;
|
||||
this.$wrapper.find('.target-block').each(function() {
|
||||
this.$wrapper.find('.es-block').each(function() {
|
||||
if ($(this).find('.selection-group').length > 0) {
|
||||
hasData = true;
|
||||
return false; // break
|
||||
@@ -1839,7 +1903,7 @@
|
||||
// Add error message after header
|
||||
var $error = $('<div>', {
|
||||
class: 'trait-validation-error',
|
||||
html: this.esIcon('warning') + ' ' + message
|
||||
html: '<i class="icon-warning"></i> ' + message
|
||||
});
|
||||
this.$wrapper.find('.condition-trait-header').after($error);
|
||||
|
||||
@@ -1849,8 +1913,8 @@
|
||||
}, 300);
|
||||
|
||||
// Expand the trait if collapsed
|
||||
if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
|
||||
this.$wrapper.find('.condition-trait-body').slideDown(200);
|
||||
if (!this.$wrapper.find('.condition-trait-body').hasClass('es-expanded')) {
|
||||
this.$wrapper.find('.condition-trait-body').addClass('es-expanded');
|
||||
this.$wrapper.removeClass('collapsed');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
var self = this;
|
||||
this.$wrapper.find('.selection-group').each(function() {
|
||||
var $group = $(this);
|
||||
var $block = $group.closest('.target-block');
|
||||
var $block = $group.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
|
||||
// Include method info
|
||||
@@ -88,18 +88,24 @@
|
||||
if (!$select.length || $select.data('methodDropdownInit')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if only 1 option (nothing to switch between)
|
||||
if ($select.find('option').length <= 1) {
|
||||
$select.data('methodDropdownInit', true);
|
||||
return;
|
||||
}
|
||||
$select.data('methodDropdownInit', true);
|
||||
|
||||
$select.addClass('method-select-hidden');
|
||||
|
||||
var $selectedOption = $select.find('option:selected');
|
||||
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
|
||||
var selectedIcon = $selectedOption.data('icon') || 'icon-caret-down';
|
||||
var selectedLabel = $selectedOption.text();
|
||||
|
||||
var triggerHtml = '<div class="method-dropdown-trigger">';
|
||||
triggerHtml += this.esIcon(selectedIcon, 'method-trigger-icon');
|
||||
triggerHtml += '<i class="' + this.escapeAttr(selectedIcon) + ' method-trigger-icon"></i>';
|
||||
triggerHtml += '<span class="method-trigger-label">' + this.escapeHtml(selectedLabel) + '</span>';
|
||||
triggerHtml += this.esIcon('arrow_drop_down', 'method-trigger-caret');
|
||||
triggerHtml += '<i class="icon-caret-down method-trigger-caret"></i>';
|
||||
triggerHtml += '</div>';
|
||||
|
||||
var $trigger = $(triggerHtml);
|
||||
@@ -127,10 +133,10 @@
|
||||
*/
|
||||
updateMethodTrigger: function($select, $trigger) {
|
||||
var $selectedOption = $select.find('option:selected');
|
||||
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
|
||||
var selectedIcon = $selectedOption.data('icon') || 'icon-caret-down';
|
||||
var selectedLabel = $selectedOption.text();
|
||||
|
||||
$trigger.find('.method-trigger-icon').replaceWith(this.esIcon(selectedIcon, 'method-trigger-icon'));
|
||||
$trigger.find('.method-trigger-icon').attr('class', selectedIcon + ' method-trigger-icon');
|
||||
$trigger.find('.method-trigger-label').text(selectedLabel);
|
||||
},
|
||||
|
||||
@@ -194,16 +200,16 @@
|
||||
// Render ungrouped options first
|
||||
$select.children('option').each(function() {
|
||||
var $el = $(this);
|
||||
var icon = $el.data('icon') || 'star';
|
||||
var icon = $el.data('icon') || 'icon-asterisk';
|
||||
var label = $el.text();
|
||||
var value = $el.val();
|
||||
var isSelected = $el.is(':selected');
|
||||
|
||||
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
|
||||
html += self.esIcon(icon, 'method-item-icon');
|
||||
html += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
|
||||
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
|
||||
if (isSelected) {
|
||||
html += self.esIcon('check', 'method-item-check');
|
||||
html += '<i class="icon-check method-item-check"></i>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
@@ -219,16 +225,16 @@
|
||||
|
||||
$optgroup.children('option').each(function() {
|
||||
var $el = $(this);
|
||||
var icon = $el.data('icon') || 'settings';
|
||||
var icon = $el.data('icon') || 'icon-cog';
|
||||
var label = $el.text();
|
||||
var value = $el.val();
|
||||
var isSelected = $el.is(':selected');
|
||||
|
||||
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
|
||||
html += self.esIcon(icon, 'method-item-icon');
|
||||
html += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
|
||||
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
|
||||
if (isSelected) {
|
||||
html += self.esIcon('check', 'method-item-check');
|
||||
html += '<i class="icon-check method-item-check"></i>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
@@ -384,13 +390,13 @@
|
||||
type: 'button',
|
||||
class: 'comb-toolbar-btn comb-select-all',
|
||||
title: trans.select_all || 'Select all',
|
||||
html: self.esIcon('check_box')
|
||||
html: '<i class="icon-check-square-o"></i>'
|
||||
}));
|
||||
$toolbar.append($('<button>', {
|
||||
type: 'button',
|
||||
class: 'comb-toolbar-btn comb-select-none',
|
||||
title: trans.clear || 'Clear',
|
||||
html: self.esIcon('check_box_outline_blank')
|
||||
html: '<i class="icon-square-o"></i>'
|
||||
}));
|
||||
$toolbar.append($('<input>', {
|
||||
type: 'text',
|
||||
@@ -404,7 +410,7 @@
|
||||
});
|
||||
$valuesContainer.append($('<span>', {
|
||||
class: 'comb-attr-loading',
|
||||
html: self.esIcon('progress_activity', 'es-spin')
|
||||
html: '<i class="icon-spinner icon-spin"></i>'
|
||||
}));
|
||||
|
||||
$groupDiv.append($groupHeader);
|
||||
@@ -582,17 +588,7 @@
|
||||
var helpContent = blockHelp[method] || this.getBuiltInMethodHelp(method);
|
||||
|
||||
if (helpContent) {
|
||||
var $infoWrapper = $('<span>', {
|
||||
class: 'mpr-info-wrapper',
|
||||
'data-details': helpContent
|
||||
});
|
||||
$infoWrapper.append($(this.esIcon('info')));
|
||||
$placeholder.append($infoWrapper);
|
||||
|
||||
// Let prestashop-admin info-tooltip.js handle this element
|
||||
if (window.MPRInfoTooltip) {
|
||||
window.MPRInfoTooltip.init();
|
||||
}
|
||||
$placeholder.append(this._buildInfoTooltip(helpContent, 'tooltip'));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -851,7 +847,7 @@
|
||||
$wrapper.addClass('selector-locked');
|
||||
if (!$wrapper.find('.lock-indicator').length) {
|
||||
var lockHtml = '<span class="mpr-info-wrapper lock-indicator">' +
|
||||
this.esIcon('lock') +
|
||||
'<i class="icon-lock"></i>' +
|
||||
'<span class="mpr-tooltip">' +
|
||||
(trans.remove_excludes_first || 'Remove all exceptions to change selection type') +
|
||||
'</span>' +
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
var self = this;
|
||||
var total = 0;
|
||||
|
||||
this.$wrapper.find('.target-block-tab .tab-badge').each(function() {
|
||||
this.$wrapper.find('.es-block-tab .tab-badge').each(function() {
|
||||
var $badge = $(this);
|
||||
if (!$badge.hasClass('loading')) {
|
||||
var count = parseInt($badge.text(), 10);
|
||||
@@ -37,7 +37,7 @@
|
||||
$countValue.text(total);
|
||||
} else {
|
||||
// Fallback: set HTML with icon
|
||||
$totalBadge.html(self.esIcon('visibility') + ' <span class="count-value">' + total + '</span>');
|
||||
$totalBadge.html('<i class="icon-eye"></i> <span class="count-value">' + total + '</span>');
|
||||
}
|
||||
$totalBadge.show();
|
||||
} else {
|
||||
@@ -52,7 +52,7 @@
|
||||
if (!$toggle.length) return;
|
||||
|
||||
var $checkbox = $toggle.find('.show-all-checkbox');
|
||||
var hasData = this.$wrapper.find('.target-block-tab.has-data').length > 0;
|
||||
var hasData = this.$wrapper.find('.es-block-tab.has-data').length > 0;
|
||||
|
||||
$checkbox.prop('checked', !hasData);
|
||||
},
|
||||
@@ -86,12 +86,12 @@
|
||||
var previewType = options.previewType || 'default';
|
||||
|
||||
// Build popover HTML
|
||||
var html = '<div class="target-preview-popover preview-type-' + previewType + '">';
|
||||
var html = '<div class="es-preview-popover preview-type-' + previewType + '">';
|
||||
|
||||
// Header with count and close button
|
||||
html += '<div class="preview-header">';
|
||||
html += '<span class="preview-count">' + totalCount + ' ' + entityLabel + '</span>';
|
||||
html += '<button type="button" class="preview-close">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="preview-close"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
// Filter input
|
||||
@@ -119,7 +119,7 @@
|
||||
html += '<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>';
|
||||
html += '</select>';
|
||||
html += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">' + remaining + '</span> ' + (trans.remaining || 'remaining') + '</span>';
|
||||
html += '<button type="button" class="btn-load-more">' + self.esIcon('add') + '</button>';
|
||||
html += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
@@ -181,7 +181,7 @@
|
||||
if ($btn.hasClass('loading')) return;
|
||||
|
||||
$btn.addClass('loading');
|
||||
$btn.find('i').replaceWith(self.esIcon('progress_activity', 'es-spin'));
|
||||
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
|
||||
$select.prop('disabled', true);
|
||||
|
||||
// Get selected load count
|
||||
@@ -236,7 +236,7 @@
|
||||
|
||||
// Reset button state
|
||||
$btn.removeClass('loading');
|
||||
$btn.find('i').replaceWith(this.esIcon('add'));
|
||||
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
|
||||
$select.prop('disabled', false);
|
||||
|
||||
// Update remaining count
|
||||
@@ -286,7 +286,7 @@
|
||||
if (item.image) {
|
||||
html += '<img src="' + this.escapeAttr(item.image) + '" class="preview-item-image" alt="">';
|
||||
} else {
|
||||
html += '<div class="preview-item-icon">' + self.esIcon('inventory_2') + '</div>';
|
||||
html += '<div class="preview-item-icon"><i class="material-icons">inventory_2</i></div>';
|
||||
}
|
||||
|
||||
// Info section
|
||||
@@ -378,7 +378,7 @@
|
||||
$list.addClass('filtering');
|
||||
// Add overlay if not exists
|
||||
if (!$list.find('.filter-loading-overlay').length) {
|
||||
$list.append('<div class="filter-loading-overlay">' + this.esIcon('progress_activity', 'es-spin') + '</div>');
|
||||
$list.append('<div class="filter-loading-overlay"><i class="icon-spinner icon-spin"></i></div>');
|
||||
}
|
||||
} else {
|
||||
$list.removeClass('filtering');
|
||||
@@ -430,7 +430,7 @@
|
||||
var $select = $controls.find('.load-more-select');
|
||||
|
||||
$btn.removeClass('loading');
|
||||
$btn.find('i').replaceWith(self.esIcon('add'));
|
||||
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
|
||||
$select.prop('disabled', false);
|
||||
$controls.find('.remaining-count').text(remaining);
|
||||
|
||||
@@ -453,7 +453,7 @@
|
||||
footerHtml += '<option value="' + remaining + '">' + (trans.all || 'All') + ' (' + remaining + ')</option>';
|
||||
footerHtml += '</select>';
|
||||
footerHtml += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">' + remaining + '</span> ' + (trans.remaining || 'remaining') + '</span>';
|
||||
footerHtml += '<button type="button" class="btn-load-more">' + self.esIcon('add') + '</button>';
|
||||
footerHtml += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
|
||||
footerHtml += '</div>';
|
||||
footerHtml += '</div>';
|
||||
|
||||
@@ -471,7 +471,7 @@
|
||||
if ($btn.hasClass('loading')) return;
|
||||
|
||||
$btn.addClass('loading');
|
||||
$btn.find('i').replaceWith(self.esIcon('progress_activity', 'es-spin'));
|
||||
$btn.find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
|
||||
$select.prop('disabled', true);
|
||||
|
||||
var loadCount = parseInt($select.val(), 10) || 20;
|
||||
@@ -743,7 +743,7 @@
|
||||
var $controls = $btn.closest('.load-more-controls');
|
||||
var $select = $controls.find('.load-more-select');
|
||||
$btn.removeClass('loading');
|
||||
$btn.find('i').replaceWith(self.esIcon('add'));
|
||||
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
|
||||
$select.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
@@ -892,7 +892,7 @@
|
||||
var $controls = $btn.closest('.load-more-controls');
|
||||
var $select = $controls.find('.load-more-select');
|
||||
$btn.removeClass('loading');
|
||||
$btn.find('i').replaceWith(self.esIcon('add'));
|
||||
$btn.find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
|
||||
$select.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
@@ -910,7 +910,7 @@
|
||||
}
|
||||
|
||||
if (!blockType) {
|
||||
var $block = $badge.closest('.target-block');
|
||||
var $block = $badge.closest('.es-block');
|
||||
blockType = $block.data('blockType') || 'products';
|
||||
}
|
||||
|
||||
@@ -1247,7 +1247,7 @@
|
||||
var isProducts = (ctx.entityType === 'categories');
|
||||
var action = isProducts ? 'previewCategoryProducts' : 'previewCategoryPages';
|
||||
|
||||
$btn.prop('disabled', true).find('i').addClass('es-spin');
|
||||
$btn.prop('disabled', true).find('i').addClass('icon-spin');
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
@@ -1263,7 +1263,7 @@
|
||||
query: this.previewFilterQuery || ''
|
||||
},
|
||||
success: function(response) {
|
||||
$btn.prop('disabled', false).find('i').removeClass('es-spin');
|
||||
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
|
||||
|
||||
if (response.success && response.items) {
|
||||
self.appendPreviewItems(response.items);
|
||||
@@ -1275,7 +1275,7 @@
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$btn.prop('disabled', false).find('i').removeClass('es-spin');
|
||||
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1335,13 +1335,13 @@
|
||||
html += '<div class="pattern-preview-modal">';
|
||||
html += '<div class="pattern-preview-header">';
|
||||
html += '<span class="pattern-preview-title">';
|
||||
html += this.esIcon('visibility') + ' ' + (trans.preview || 'Preview') + ': <code>' + this.escapeHtml(pattern) + '</code>';
|
||||
html += '<i class="icon-eye"></i> ' + (trans.preview || 'Preview') + ': <code>' + this.escapeHtml(pattern) + '</code>';
|
||||
html += '</span>';
|
||||
html += '<span class="pattern-preview-count">' + count + ' ' + (count === 1 ? entityLabelSingular : entityLabelPlural) + '</span>';
|
||||
html += '<button type="button" class="pattern-preview-close">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="pattern-preview-close"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
html += '<div class="pattern-preview-content">';
|
||||
html += '<div class="pattern-preview-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
|
||||
html += '<div class="pattern-preview-loading"><i class="icon-spinner icon-spin"></i> ' + (trans.loading || 'Loading...') + '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
@@ -1414,10 +1414,71 @@
|
||||
// =========================================================================
|
||||
|
||||
refreshGroupPreviewIfOpen: function($group) {
|
||||
// Check if preview is for this group and refresh if needed
|
||||
if (!this.$activeBadge || !this.$previewPopover) {
|
||||
return;
|
||||
}
|
||||
var $badge = $group.find('.group-count-badge.popover-open, .group-preview-badge.popover-open');
|
||||
if (!$badge.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var $block = $badge.closest('.es-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
var groupData = this.serializeGroup($group, blockType);
|
||||
|
||||
if (!groupData || !groupData.include) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state in list
|
||||
var $list = this.$previewPopover.find('.preview-list');
|
||||
$list.css('opacity', '0.5');
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'previewGroupItems',
|
||||
trait: 'EntitySelector',
|
||||
group_data: JSON.stringify(groupData),
|
||||
block_type: blockType,
|
||||
limit: 10
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && self.$previewPopover) {
|
||||
// Update count in header
|
||||
var blockConfig = self.config.blocks && self.config.blocks[blockType] ? self.config.blocks[blockType] : {};
|
||||
var entityLabel = blockConfig.entity_label_plural || 'products';
|
||||
self.$previewPopover.find('.preview-header .preview-count').text(response.count + ' ' + entityLabel);
|
||||
|
||||
// Rebuild list items using existing renderer
|
||||
$list.html(self.renderPreviewItems(response.items || [])).css('opacity', '1');
|
||||
|
||||
// Update context
|
||||
if (self.previewContext) {
|
||||
self.previewContext.groupData = groupData;
|
||||
}
|
||||
|
||||
// Update load more
|
||||
var $footer = self.$previewPopover.find('.preview-footer');
|
||||
if (response.hasMore) {
|
||||
var remaining = response.count - (response.items || []).length;
|
||||
$footer.find('.remaining-count').text(remaining);
|
||||
$footer.show();
|
||||
} else {
|
||||
$footer.hide();
|
||||
}
|
||||
} else {
|
||||
$list.css('opacity', '1');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$list.css('opacity', '1');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1463,7 +1524,7 @@
|
||||
|
||||
// Collect all entity types with data
|
||||
var summaryItems = [];
|
||||
this.$wrapper.find('.target-block-tab.has-data').each(function() {
|
||||
this.$wrapper.find('.es-block-tab.has-data').each(function() {
|
||||
var $tab = $(this);
|
||||
var blockType = $tab.data('blockType');
|
||||
var $tabBadge = $tab.find('.tab-badge');
|
||||
@@ -1472,7 +1533,7 @@
|
||||
|
||||
if (count > 0) {
|
||||
var blockConfig = self.config.blocks && self.config.blocks[blockType] ? self.config.blocks[blockType] : {};
|
||||
var icon = $tab.find('.tab-label').prev('i').text() || 'widgets';
|
||||
var icon = $tab.find('.tab-label').prev('i').attr('class') || 'icon-cube';
|
||||
var label = $tab.find('.tab-label').text() || blockType;
|
||||
|
||||
summaryItems.push({
|
||||
@@ -1486,7 +1547,7 @@
|
||||
|
||||
// Build popover HTML
|
||||
var totalCount = parseInt($badge.find('.count-value').text(), 10) || 0;
|
||||
var popoverHtml = '<div class="target-preview-popover total-preview-popover">';
|
||||
var popoverHtml = '<div class="es-preview-popover total-preview-popover">';
|
||||
popoverHtml += '<div class="preview-popover-header">';
|
||||
popoverHtml += '<span class="preview-popover-title">' + (trans.total_summary || 'Selection Summary') + '</span>';
|
||||
popoverHtml += '<span class="preview-popover-count">' + totalCount + ' ' + (trans.total_items || 'total items') + '</span>';
|
||||
@@ -1497,7 +1558,7 @@
|
||||
for (var i = 0; i < summaryItems.length; i++) {
|
||||
var item = summaryItems[i];
|
||||
popoverHtml += '<li class="total-summary-item" data-block-type="' + item.blockType + '">';
|
||||
popoverHtml += this.esIcon(item.icon);
|
||||
popoverHtml += '<i class="' + self.escapeAttr(item.icon) + '"></i>';
|
||||
popoverHtml += '<span class="summary-item-label">' + self.escapeHtml(item.label) + '</span>';
|
||||
popoverHtml += '<span class="summary-item-count">' + item.count + '</span>';
|
||||
popoverHtml += '</li>';
|
||||
@@ -1562,7 +1623,7 @@
|
||||
$('.holiday-preview-popover').remove();
|
||||
|
||||
// Create popover HTML
|
||||
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
|
||||
var popoverHtml = '<div class="holiday-preview-popover es-preview-popover show">';
|
||||
popoverHtml += '<div class="popover-header">';
|
||||
popoverHtml += '<span class="popover-title">';
|
||||
if (countryIso) {
|
||||
@@ -1570,10 +1631,10 @@
|
||||
}
|
||||
popoverHtml += this.escapeHtml(countryName) + ' - ' + (trans.holidays || 'Holidays');
|
||||
popoverHtml += '</span>';
|
||||
popoverHtml += '<button type="button" class="popover-close">' + this.esIcon('close') + '</button>';
|
||||
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="popover-body">';
|
||||
popoverHtml += '<div class="holiday-preview-loading">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
|
||||
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '</div>';
|
||||
|
||||
@@ -1660,7 +1721,7 @@
|
||||
$popover.find('.popover-body').html(listHtml);
|
||||
} else {
|
||||
var noDataHtml = '<div class="holiday-preview-empty">';
|
||||
noDataHtml += self.esIcon('event_busy');
|
||||
noDataHtml += '<i class="material-icons">event_busy</i>';
|
||||
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
|
||||
noDataHtml += '</div>';
|
||||
$popover.find('.popover-body').html(noDataHtml);
|
||||
@@ -1677,7 +1738,7 @@
|
||||
},
|
||||
error: function() {
|
||||
var errorHtml = '<div class="holiday-preview-empty">';
|
||||
errorHtml += self.esIcon('error');
|
||||
errorHtml += '<i class="material-icons">error_outline</i>';
|
||||
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
|
||||
errorHtml += '</div>';
|
||||
$popover.find('.popover-body').html(errorHtml);
|
||||
@@ -1703,17 +1764,17 @@
|
||||
$('.holiday-preview-popover').remove();
|
||||
|
||||
// Create popover HTML with placeholder title (will update after AJAX)
|
||||
var popoverHtml = '<div class="holiday-preview-popover target-preview-popover show">';
|
||||
var popoverHtml = '<div class="holiday-preview-popover es-preview-popover show">';
|
||||
popoverHtml += '<div class="popover-header">';
|
||||
popoverHtml += '<span class="popover-title">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</span>';
|
||||
popoverHtml += '<button type="button" class="popover-close">' + this.esIcon('close') + '</button>';
|
||||
popoverHtml += '<span class="popover-title"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</span>';
|
||||
popoverHtml += '<button type="button" class="popover-close"><i class="material-icons">close</i></button>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="popover-filter">';
|
||||
popoverHtml += this.esIcon('search');
|
||||
popoverHtml += '<i class="material-icons">search</i>';
|
||||
popoverHtml += '<input type="text" class="holiday-filter-input" placeholder="' + (trans.filter_holidays || 'Filter by country, date, name...') + '">';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="popover-body">';
|
||||
popoverHtml += '<div class="holiday-preview-loading">' + this.esIcon('sync', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '</div>';
|
||||
popoverHtml += '<div class="holiday-preview-loading"><i class="material-icons icon-spin">sync</i> ' + (trans.loading || 'Loading...') + '</div>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '</div>';
|
||||
|
||||
@@ -1901,7 +1962,7 @@
|
||||
$popover.find('.popover-title').html('0 ' + (trans.holidays || 'Holidays'));
|
||||
|
||||
var noDataHtml = '<div class="holiday-preview-empty">';
|
||||
noDataHtml += self.esIcon('event_busy');
|
||||
noDataHtml += '<i class="material-icons">event_busy</i>';
|
||||
noDataHtml += '<p>' + (trans.no_holidays || 'No holidays found') + '</p>';
|
||||
noDataHtml += '</div>';
|
||||
$popover.find('.popover-body').html(noDataHtml);
|
||||
@@ -1918,10 +1979,10 @@
|
||||
},
|
||||
error: function() {
|
||||
// Update header for error state
|
||||
$popover.find('.popover-title').html(self.esIcon('error') + ' ' + (trans.error || 'Error'));
|
||||
$popover.find('.popover-title').html('<i class="material-icons">error_outline</i> ' + (trans.error || 'Error'));
|
||||
|
||||
var errorHtml = '<div class="holiday-preview-empty">';
|
||||
errorHtml += self.esIcon('error');
|
||||
errorHtml += '<i class="material-icons">error_outline</i>';
|
||||
errorHtml += '<p>' + (trans.error_loading || 'Error loading holidays') + '</p>';
|
||||
errorHtml += '</div>';
|
||||
$popover.find('.popover-body').html(errorHtml);
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
var selectedIds = [];
|
||||
var hiddenIds = [];
|
||||
if (this.activeGroup) {
|
||||
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $block = this.$wrapper.find('.es-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
|
||||
var currentSearchEntity = this.activeGroup.searchEntity;
|
||||
var currentExcludeIndex = this.activeGroup.excludeIndex;
|
||||
@@ -361,7 +361,7 @@
|
||||
|
||||
var html = '';
|
||||
if (visibleResults.length === 0 && !appendMode) {
|
||||
html = '<div class="no-results">' + this.esIcon('search') + ' ' + (trans.no_results || 'No results found') + '</div>';
|
||||
html = '<div class="no-results"><i class="icon-search"></i> ' + (trans.no_results || 'No results found') + '</div>';
|
||||
} else {
|
||||
visibleResults.forEach(function(item) {
|
||||
var isSelected = selectedIds.indexOf(String(item.id)) !== -1;
|
||||
@@ -376,27 +376,27 @@
|
||||
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
|
||||
html += '>';
|
||||
|
||||
html += '<span class="result-checkbox">' + self.esIcon('check') + '</span>';
|
||||
html += '<span class="result-checkbox"><i class="icon-check"></i></span>';
|
||||
|
||||
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : null;
|
||||
|
||||
// Countries show flags
|
||||
if (searchEntity === 'countries' && item.iso_code) {
|
||||
var flagUrl = 'https://flagcdn.com/w40/' + item.iso_code.toLowerCase() + '.png';
|
||||
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;">' + self.esIcon('flag') + '</span></div>';
|
||||
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;"><i class="icon-flag"></i></span></div>';
|
||||
} else if (item.image) {
|
||||
html += '<div class="result-image"><img src="' + self.escapeAttr(item.image) + '" alt=""></div>';
|
||||
} else {
|
||||
// Entity-specific icons
|
||||
var iconName = 'widgets'; // default
|
||||
if (searchEntity === 'categories') iconName = 'folder';
|
||||
else if (searchEntity === 'manufacturers') iconName = 'business';
|
||||
else if (searchEntity === 'suppliers') iconName = 'local_shipping';
|
||||
else if (searchEntity === 'attributes') iconName = 'brush';
|
||||
else if (searchEntity === 'features') iconName = 'list';
|
||||
else if (searchEntity === 'cms') iconName = 'description';
|
||||
else if (searchEntity === 'cms_categories') iconName = 'folder';
|
||||
html += '<div class="result-icon">' + self.esIcon(iconName) + '</div>';
|
||||
var iconClass = 'icon-cube'; // default
|
||||
if (searchEntity === 'categories') iconClass = 'icon-folder';
|
||||
else if (searchEntity === 'manufacturers') iconClass = 'icon-building';
|
||||
else if (searchEntity === 'suppliers') iconClass = 'icon-truck';
|
||||
else if (searchEntity === 'attributes') iconClass = 'icon-paint-brush';
|
||||
else if (searchEntity === 'features') iconClass = 'icon-list-ul';
|
||||
else if (searchEntity === 'cms') iconClass = 'icon-file-text-o';
|
||||
else if (searchEntity === 'cms_categories') iconClass = 'icon-folder-o';
|
||||
html += '<div class="result-icon"><i class="' + iconClass + '"></i></div>';
|
||||
}
|
||||
|
||||
html += '<div class="result-info">';
|
||||
@@ -588,10 +588,10 @@
|
||||
for (var i = 0; i < history.length; i++) {
|
||||
var query = history[i];
|
||||
html += '<div class="history-item" data-query="' + this.escapeAttr(query) + '">';
|
||||
html += this.esIcon('schedule');
|
||||
html += '<i class="icon-clock-o"></i>';
|
||||
html += '<span class="history-query">' + this.escapeHtml(query) + '</span>';
|
||||
html += '<button type="button" class="btn-delete-history" title="' + (trans.remove || 'Remove') + '">';
|
||||
html += this.esIcon('close');
|
||||
html += '<i class="icon-times"></i>';
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
var searchEntity = this.activeGroup ? this.activeGroup.searchEntity : 'categories';
|
||||
|
||||
// Show loading
|
||||
$results.html('<div class="tree-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' +
|
||||
$results.html('<div class="tree-loading"><i class="icon-spinner icon-spin"></i> ' +
|
||||
this.escapeHtml(trans.loading || 'Loading...') + '</div>');
|
||||
|
||||
// Fetch tree data
|
||||
@@ -115,11 +115,11 @@
|
||||
html += '<div class="tree-toolbar">';
|
||||
html += '<button type="button" class="btn-expand-all" title="' +
|
||||
this.escapeAttr(trans.expand_all || 'Expand all') + '">';
|
||||
html += this.esIcon('add_box') + ' ' + this.escapeHtml(trans.expand_all || 'Expand all');
|
||||
html += '<i class="icon-plus-square-o"></i> ' + this.escapeHtml(trans.expand_all || 'Expand all');
|
||||
html += '</button>';
|
||||
html += '<button type="button" class="btn-collapse-all" title="' +
|
||||
this.escapeAttr(trans.collapse_all || 'Collapse all') + '">';
|
||||
html += this.esIcon('indeterminate_check_box') + ' ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
|
||||
html += '<i class="icon-minus-square-o"></i> ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -178,21 +178,21 @@
|
||||
|
||||
// Toggle button (expand/collapse)
|
||||
if (hasChildren) {
|
||||
html += '<span class="tree-toggle">' + self.esIcon('arrow_drop_down') + '</span>';
|
||||
html += '<span class="tree-toggle"><i class="icon-caret-down"></i></span>';
|
||||
// Select with children button (next to toggle on the left)
|
||||
html += '<button type="button" class="btn-select-children" title="' +
|
||||
self.escapeAttr(trans.select_with_children || 'Select with all children') + '">';
|
||||
html += self.esIcon('check_box');
|
||||
html += '<i class="icon-check-square-o"></i>';
|
||||
html += '</button>';
|
||||
} else {
|
||||
html += '<span class="tree-toggle tree-leaf"></span>';
|
||||
}
|
||||
|
||||
// Checkbox indicator
|
||||
html += '<span class="tree-checkbox">' + self.esIcon('check') + '</span>';
|
||||
html += '<span class="tree-checkbox"><i class="icon-check"></i></span>';
|
||||
|
||||
// Category icon
|
||||
html += '<span class="tree-icon">' + self.esIcon('folder') + '</span>';
|
||||
html += '<span class="tree-icon"><i class="icon-folder"></i></span>';
|
||||
|
||||
// Name
|
||||
html += '<span class="tree-name">' + self.escapeHtml(node.name) + '</span>';
|
||||
@@ -203,7 +203,7 @@
|
||||
var countLabel = node.page_count ? (trans.pages || 'pages') : (trans.products || 'products');
|
||||
html += '<span class="tree-count clickable" data-category-id="' + node.id + '" ';
|
||||
html += 'title="' + self.escapeAttr(itemCount + ' ' + countLabel) + '">';
|
||||
html += self.esIcon('visibility') + ' ' + itemCount;
|
||||
html += '<i class="icon-eye"></i> ' + itemCount;
|
||||
html += '</span>';
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
if (!this.activeGroup) return selectedIds;
|
||||
|
||||
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $block = this.$wrapper.find('.es-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
|
||||
@@ -346,10 +346,10 @@
|
||||
});
|
||||
|
||||
if (isParentSelected && allChildrenSelected) {
|
||||
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
|
||||
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
|
||||
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
|
||||
} else {
|
||||
$btn.find('i').replaceWith(self.esIcon('add_box'));
|
||||
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
|
||||
$btn.attr('title', trans.select_with_children || 'Select with all children');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,97 +19,6 @@
|
||||
// Create mixin namespace
|
||||
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Icon framework detection & FA4 mapping (module-level singleton)
|
||||
// ---------------------------------------------------------------
|
||||
var _iconMode = null;
|
||||
|
||||
/**
|
||||
* Material Icons → FontAwesome 4 class mapping.
|
||||
* FA4 uses class-based icons (icon-name), Material uses text content.
|
||||
*/
|
||||
var FA4_MAP = {
|
||||
'account_tree': 'icon-sitemap',
|
||||
'add': 'icon-plus',
|
||||
'add_box': 'icon-plus-square',
|
||||
'arrow_downward': 'icon-sort-desc',
|
||||
'arrow_drop_down': 'icon-caret-down',
|
||||
'arrow_right': 'icon-chevron-right',
|
||||
'arrow_upward': 'icon-sort-asc',
|
||||
'block': 'icon-ban',
|
||||
'brush': 'icon-paint-brush',
|
||||
'business': 'icon-building',
|
||||
'check': 'icon-check',
|
||||
'check_box': 'icon-check-square',
|
||||
'check_box_outline_blank': 'icon-square-o',
|
||||
'check_circle': 'icon-check-circle',
|
||||
'close': 'icon-times',
|
||||
'delete': 'icon-trash',
|
||||
'description': 'icon-file-text',
|
||||
'error': 'icon-exclamation-circle',
|
||||
'event': 'icon-calendar',
|
||||
'event_busy': 'icon-calendar-times-o',
|
||||
'expand_less': 'icon-chevron-up',
|
||||
'expand_more': 'icon-chevron-down',
|
||||
'filter_list': 'icon-filter',
|
||||
'flag': 'icon-flag',
|
||||
'folder': 'icon-folder',
|
||||
'folder_open': 'icon-folder-open',
|
||||
'indeterminate_check_box': 'icon-minus-square',
|
||||
'info': 'icon-info-circle',
|
||||
'inventory_2': 'icon-archive',
|
||||
'label': 'icon-tag',
|
||||
'language': 'icon-globe',
|
||||
'lightbulb': 'icon-lightbulb-o',
|
||||
'list': 'icon-list',
|
||||
'list_alt': 'icon-list-alt',
|
||||
'local_shipping': 'icon-truck',
|
||||
'lock': 'icon-lock',
|
||||
'my_location': 'icon-crosshairs',
|
||||
'open_in_full': 'icon-expand',
|
||||
'payments': 'icon-credit-card',
|
||||
'progress_activity': 'icon-circle-o-notch',
|
||||
'schedule': 'icon-clock-o',
|
||||
'search': 'icon-search',
|
||||
'shopping_cart': 'icon-shopping-cart',
|
||||
'shuffle': 'icon-random',
|
||||
'sort': 'icon-sort',
|
||||
'sort_by_alpha': 'icon-sort-alpha-asc',
|
||||
'star': 'icon-star',
|
||||
'sync': 'icon-refresh',
|
||||
'tune': 'icon-sliders',
|
||||
'visibility': 'icon-eye',
|
||||
'warning': 'icon-warning',
|
||||
'widgets': 'icon-th-large'
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect icon framework: 'material' (PS 8+/9+) or 'fa4' (PS 1.6/1.7).
|
||||
* Checks PHP-set data attribute first, falls back to font detection.
|
||||
*/
|
||||
function detectIconMode() {
|
||||
if (_iconMode !== null) return _iconMode;
|
||||
|
||||
// 1. PHP sets data-icon-mode on the wrapper
|
||||
var $w = $('.entity-selector-trait[data-icon-mode], .target-conditions-trait[data-icon-mode]').first();
|
||||
if ($w.length && $w.data('icon-mode')) {
|
||||
_iconMode = $w.data('icon-mode');
|
||||
return _iconMode;
|
||||
}
|
||||
|
||||
// 2. Fallback: probe whether Material Icons font is loaded
|
||||
var test = document.createElement('i');
|
||||
test.className = 'material-icons';
|
||||
test.style.cssText = 'position:absolute;left:-9999px;top:-9999px;font-size:16px;pointer-events:none';
|
||||
test.textContent = 'check';
|
||||
(document.body || document.documentElement).appendChild(test);
|
||||
var family = (window.getComputedStyle(test).fontFamily || '').toLowerCase();
|
||||
test.parentNode.removeChild(test);
|
||||
|
||||
_iconMode = (family.indexOf('material') !== -1) ? 'material' : 'fa4';
|
||||
return _iconMode;
|
||||
}
|
||||
|
||||
// Utility functions mixin
|
||||
window._EntitySelectorMixins.utils = {
|
||||
|
||||
@@ -151,62 +60,18 @@
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
/**
|
||||
* Icon helper — returns HTML for an icon that works on PS 1.6 through 9.x.
|
||||
* Automatically uses Material Icons (PS 8+/9+) or FontAwesome 4 (PS 1.6/1.7).
|
||||
*
|
||||
* @param {string} name - Canonical icon name (Material Icons name, e.g. 'lock', 'search', 'delete')
|
||||
* @param {string} [extraClass] - Additional CSS class(es) (e.g. 'es-spin', 'method-trigger-icon')
|
||||
* @returns {string} HTML string for an <i> element
|
||||
*/
|
||||
esIcon: function(name, extraClass) {
|
||||
var mode = detectIconMode();
|
||||
if (mode === 'material') {
|
||||
var cls = 'material-icons es-icon';
|
||||
if (extraClass) cls += ' ' + extraClass;
|
||||
return '<i class="' + cls + '">' + name + '</i>';
|
||||
}
|
||||
// FA4: icon is encoded in the class name, no text content
|
||||
var mapped = FA4_MAP[name] || 'icon-circle';
|
||||
var cls = mapped + ' es-icon';
|
||||
if (extraClass) cls += ' ' + extraClass;
|
||||
return '<i class="' + cls + '"></i>';
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing <i> icon element to show a different icon.
|
||||
* Handles both Material Icons and FA4 modes.
|
||||
*
|
||||
* @param {jQuery} $el - The <i> element to update
|
||||
* @param {string} name - Canonical icon name
|
||||
* @param {string} [extraClass] - Additional CSS class(es) to preserve
|
||||
*/
|
||||
esIconUpdate: function($el, name, extraClass) {
|
||||
var mode = detectIconMode();
|
||||
if (mode === 'material') {
|
||||
var cls = 'material-icons es-icon';
|
||||
if (extraClass) cls += ' ' + extraClass;
|
||||
$el.attr('class', cls).text(name);
|
||||
} else {
|
||||
var mapped = FA4_MAP[name] || 'icon-circle';
|
||||
var cls = mapped + ' es-icon';
|
||||
if (extraClass) cls += ' ' + extraClass;
|
||||
$el.attr('class', cls).text('');
|
||||
}
|
||||
},
|
||||
|
||||
getEntityTypeIcon: function(entityType) {
|
||||
var icons = {
|
||||
'products': 'shopping_cart',
|
||||
'categories': 'folder_open',
|
||||
'manufacturers': 'business',
|
||||
'suppliers': 'local_shipping',
|
||||
'attributes': 'list_alt',
|
||||
'features': 'label',
|
||||
'cms': 'description',
|
||||
'cms_categories': 'folder'
|
||||
'products': 'icon-shopping-cart',
|
||||
'categories': 'icon-folder-open',
|
||||
'manufacturers': 'icon-building',
|
||||
'suppliers': 'icon-truck',
|
||||
'attributes': 'icon-list-alt',
|
||||
'features': 'icon-tags',
|
||||
'cms': 'icon-file-text',
|
||||
'cms_categories': 'icon-folder'
|
||||
};
|
||||
return icons[entityType] || 'widgets';
|
||||
return icons[entityType] || 'icon-cube';
|
||||
},
|
||||
|
||||
getEntityTypeLabel: function(entityType) {
|
||||
@@ -229,7 +94,7 @@
|
||||
if (!isRequired) return true;
|
||||
|
||||
var hasData = false;
|
||||
this.$wrapper.find('.target-block').each(function() {
|
||||
this.$wrapper.find('.es-block').each(function() {
|
||||
if ($(this).find('.selection-group').length > 0) {
|
||||
hasData = true;
|
||||
return false;
|
||||
@@ -251,12 +116,12 @@
|
||||
this.$wrapper.find('.trait-validation-error').remove();
|
||||
var $error = $('<div>', {
|
||||
class: 'trait-validation-error',
|
||||
html: this.esIcon('warning') + ' ' + message
|
||||
html: '<i class="icon-warning"></i> ' + message
|
||||
});
|
||||
this.$wrapper.find('.condition-trait-header').after($error);
|
||||
$('html, body').animate({ scrollTop: this.$wrapper.offset().top - 100 }, 300);
|
||||
if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
|
||||
this.$wrapper.find('.condition-trait-body').slideDown(200);
|
||||
if (!this.$wrapper.find('.condition-trait-body').hasClass('es-expanded')) {
|
||||
this.$wrapper.find('.condition-trait-body').addClass('es-expanded');
|
||||
this.$wrapper.removeClass('collapsed');
|
||||
}
|
||||
},
|
||||
@@ -275,24 +140,119 @@
|
||||
return this.getBlockMode(blockType) === 'single';
|
||||
},
|
||||
|
||||
getCurrentSingleSelection: function() {
|
||||
if ((this.config.mode || 'multi') !== 'single') return null;
|
||||
var $chip = this.$wrapper.find('.entity-chips .entity-chip').first();
|
||||
if ($chip.length) {
|
||||
var $block = $chip.closest('.target-block');
|
||||
return {
|
||||
name: $chip.find('.chip-name').text() || $chip.data('id'),
|
||||
entityType: $block.data('block-type') || 'item'
|
||||
};
|
||||
getCurrentSingleSelection: function(blockType) {
|
||||
// Global single mode — check any chip across all blocks
|
||||
if ((this.config.mode || 'multi') === 'single') {
|
||||
var $chip = this.$wrapper.find('.entity-chips .entity-chip').first();
|
||||
if ($chip.length) {
|
||||
var $block = $chip.closest('.es-block');
|
||||
return {
|
||||
name: $chip.find('.chip-name').text() || $chip.data('id'),
|
||||
entityType: $block.data('block-type') || 'item'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Per-block single mode — check active block or specified blockType
|
||||
if (blockType) {
|
||||
if (this.getBlockMode(blockType) !== 'single') return null;
|
||||
var $block = this.$wrapper.find('.es-block[data-block-type="' + blockType + '"]');
|
||||
var $chip = $block.find('.entity-chips .entity-chip').first();
|
||||
if ($chip.length) {
|
||||
return {
|
||||
name: $chip.find('.chip-name').text() || $chip.data('id'),
|
||||
entityType: blockType
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
showReplaceConfirmation: function(currentSelection, newSelection, onConfirm) {
|
||||
// Close the search dropdown so modal is accessible
|
||||
if (typeof this.hideDropdown === 'function') {
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
||||
if (typeof MPRModal === 'undefined') {
|
||||
if (confirm('Replace "' + currentSelection.name + '" with "' + newSelection.name + '"?')) {
|
||||
onConfirm();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var t = this.config.trans || {};
|
||||
var currentTypeLabel = this.getEntityTypeLabel(currentSelection.entityType);
|
||||
var newTypeLabel = this.getEntityTypeLabel(newSelection.entityType);
|
||||
|
||||
var modal = MPRModal.create({ id: 'mpr-entity-replace-modal' });
|
||||
modal.setHeader('warning', 'swap_horiz', t.replace_title || 'Replace selection?');
|
||||
modal.setBody(
|
||||
'<p class="mpr-replace-message">' + this.escapeHtml(t.replace_message || 'Only one item is allowed. Replace the current selection?') + '</p>' +
|
||||
'<div class="mpr-replace-item mpr-replace-current">' +
|
||||
'<div class="mpr-replace-label">' + this.escapeHtml(t.replace_current || 'Current') + '</div>' +
|
||||
'<div class="mpr-replace-value">' + this.escapeHtml(currentTypeLabel) + ': ' + this.escapeHtml(currentSelection.name) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="mpr-replace-arrow"><i class="material-icons">arrow_downward</i></div>' +
|
||||
'<div class="mpr-replace-item mpr-replace-new">' +
|
||||
'<div class="mpr-replace-label">' + this.escapeHtml(t.replace_new || 'New') + '</div>' +
|
||||
'<div class="mpr-replace-value">' + this.escapeHtml(newTypeLabel) + ': ' + this.escapeHtml(newSelection.name) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
modal.setFooter([
|
||||
{ type: 'cancel', label: t.cancel || 'Cancel' },
|
||||
{ type: 'primary', label: t.replace || 'Replace', icon: 'check', onClick: function() {
|
||||
modal.hide();
|
||||
onConfirm();
|
||||
}}
|
||||
]);
|
||||
modal.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if entity type supports tree browsing
|
||||
*/
|
||||
supportsTreeBrowsing: function(entityType) {
|
||||
return entityType === 'categories' || entityType === 'cms_categories';
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a standardized empty state element
|
||||
* @param {string} text - Message to display
|
||||
* @param {string} [icon] - Optional icon class (e.g. 'icon-info-circle')
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
_buildEmptyState: function(text, icon) {
|
||||
var iconHtml = icon ? '<i class="' + this.escapeAttr(icon) + '"></i> ' : '';
|
||||
return '<span class="es-empty-state">' + iconHtml + this.escapeHtml(text) + '</span>';
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a standardized search box (icon + input + spinner)
|
||||
* @param {string} placeholder - Input placeholder text
|
||||
* @param {string} [extraClass] - Optional additional CSS class
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
_buildSearchBoxHtml: function(placeholder, extraClass) {
|
||||
var cls = extraClass ? ' ' + extraClass : '';
|
||||
return '<div class="entity-search-box' + cls + '">' +
|
||||
'<i class="icon-search entity-search-icon"></i>' +
|
||||
'<input type="text" class="entity-search-input" placeholder="' + this.escapeAttr(placeholder) + '" autocomplete="off">' +
|
||||
'<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>' +
|
||||
'</div>';
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a standardized info tooltip
|
||||
* @param {string} content - Tooltip content
|
||||
* @param {string} [type] - 'details' for data-details attr, default uses data-tooltip
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
_buildInfoTooltip: function(content, type) {
|
||||
var attr = (type === 'details') ? 'data-details' : 'data-tooltip';
|
||||
return '<span class="mpr-info-wrapper" ' + attr + '="' + this.escapeAttr(content) + '">' +
|
||||
'<i class="material-icons">info_outline</i></span>';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
var trans = this.config.trans || {};
|
||||
id = parseInt(id, 10);
|
||||
|
||||
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $block = this.$wrapper.find('.es-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
|
||||
|
||||
// Get include chips
|
||||
@@ -297,12 +297,12 @@
|
||||
|
||||
// Create toast HTML
|
||||
var html = '<div class="es-validation-toast">';
|
||||
html += '<div class="es-toast-icon">' + this.esIcon('warning') + '</div>';
|
||||
html += '<div class="es-toast-icon"><i class="icon-exclamation-triangle"></i></div>';
|
||||
html += '<div class="es-toast-content">';
|
||||
html += '<div class="es-toast-title">' + this.escapeHtml(title) + '</div>';
|
||||
html += '<div class="es-toast-message">' + this.escapeHtml(message) + '</div>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="es-toast-close">' + this.esIcon('close') + '</button>';
|
||||
html += '<button type="button" class="es-toast-close"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
var $toast = $(html);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Chips container wrapper with toolbar
|
||||
@@ -332,7 +331,7 @@
|
||||
color: darken($es-primary, 10%);
|
||||
}
|
||||
|
||||
i {
|
||||
i.material-icons {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -343,16 +342,6 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chip-attrs {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
margin-left: 2px;
|
||||
|
||||
&::before {
|
||||
content: '— ';
|
||||
}
|
||||
}
|
||||
|
||||
.chip-remove {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
@@ -831,7 +820,7 @@
|
||||
color: $es-text-secondary;
|
||||
}
|
||||
|
||||
i {
|
||||
i.material-icons {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -853,7 +842,7 @@
|
||||
color: $es-text-muted;
|
||||
font-size: $es-font-size-sm;
|
||||
|
||||
i {
|
||||
i.material-icons {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@@ -868,7 +857,7 @@
|
||||
padding: $es-spacing-xl 0;
|
||||
color: $es-text-muted;
|
||||
|
||||
i {
|
||||
i.material-icons {
|
||||
font-size: 48px;
|
||||
opacity: 0.4;
|
||||
margin-bottom: $es-spacing-sm;
|
||||
@@ -978,7 +967,7 @@
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
background: $es-slate-50;
|
||||
|
||||
i {
|
||||
i.material-icons {
|
||||
font-size: 18px;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
@@ -1015,7 +1004,6 @@
|
||||
#content.bootstrap,
|
||||
#content .bootstrap,
|
||||
.bootstrap #content {
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
.chips-wrapper .chips-toolbar {
|
||||
// Double class for extra specificity
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Main container
|
||||
|
||||
@@ -6,16 +6,15 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Search wrapper
|
||||
.target-search-wrapper {
|
||||
.es-search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Search dropdown
|
||||
.target-search-dropdown {
|
||||
.es-search-dropdown {
|
||||
@include dropdown-container;
|
||||
display: none;
|
||||
width: 600px;
|
||||
@@ -117,7 +116,7 @@
|
||||
|
||||
// Results container
|
||||
.dropdown-results {
|
||||
padding: 0 $es-spacing-sm;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Results count text
|
||||
@@ -159,12 +158,12 @@
|
||||
}
|
||||
|
||||
// Result item (both class names for compatibility)
|
||||
// Note: Main dropdown-item styling is in the global .target-search-dropdown section below
|
||||
// Note: Main dropdown-item styling is in the global .es-search-dropdown section below
|
||||
.dropdown-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm 0;
|
||||
padding: $es-spacing-sm;
|
||||
background: $es-white;
|
||||
border: none;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
@@ -359,20 +358,6 @@
|
||||
gap: $es-spacing-sm;
|
||||
}
|
||||
|
||||
// Combination-level search results ('both' mode)
|
||||
.dropdown-item.is-combination {
|
||||
padding-left: 28px;
|
||||
|
||||
.result-name {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item.is-parent-product {
|
||||
background: $es-slate-50;
|
||||
font-weight: $es-font-weight-medium;
|
||||
}
|
||||
|
||||
// No results state
|
||||
.no-results {
|
||||
display: flex;
|
||||
@@ -616,7 +601,6 @@
|
||||
}
|
||||
|
||||
// Category tree view
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
.category-tree {
|
||||
padding: $es-spacing-sm;
|
||||
@@ -884,8 +868,8 @@
|
||||
// Global dropdown styles (when appended to body instead of inside wrapper)
|
||||
// Duplicates key styles for when dropdown is outside .entity-selector-trait
|
||||
// =============================================================================
|
||||
body > .target-search-dropdown,
|
||||
.target-search-dropdown {
|
||||
body > .es-search-dropdown,
|
||||
.es-search-dropdown {
|
||||
@include dropdown-container;
|
||||
display: none;
|
||||
width: 600px;
|
||||
@@ -1304,9 +1288,50 @@ body > .target-search-dropdown,
|
||||
font-weight: $es-font-weight-medium;
|
||||
}
|
||||
|
||||
// Count with eye icon (like group-count-badge)
|
||||
.toggle-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
color: $es-text-muted;
|
||||
font-size: 0.65rem;
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
// Clickable preview badge
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: rgba($es-primary, 0.1);
|
||||
color: $es-primary;
|
||||
|
||||
i {
|
||||
color: $es-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.popover-open {
|
||||
background: $es-primary;
|
||||
color: $es-white;
|
||||
|
||||
i {
|
||||
color: $es-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
i {
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1399,67 +1424,6 @@ body > .target-search-dropdown,
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
// Filter chip wrapper (chip/toggle + preview button)
|
||||
.filter-chip-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
border-radius: $es-radius-sm;
|
||||
overflow: hidden;
|
||||
|
||||
// Left element gets left-only border-radius
|
||||
.filter-chip,
|
||||
.filter-group-toggle {
|
||||
border-radius: $es-radius-sm 0 0 $es-radius-sm;
|
||||
}
|
||||
|
||||
// Preview eye button — unified for both value chips and group toggles
|
||||
.chip-preview-btn {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0.375rem;
|
||||
font-size: 10px;
|
||||
color: $es-text-muted;
|
||||
background: $es-slate-100;
|
||||
border-left: 1px solid $es-border-color;
|
||||
border-radius: 0 $es-radius-sm $es-radius-sm 0;
|
||||
cursor: pointer;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: rgba($es-primary, 0.1);
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
&.popover-open {
|
||||
background: $es-primary;
|
||||
color: $es-white;
|
||||
}
|
||||
|
||||
&.loading i {
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
// When no preview button, restore full border-radius
|
||||
.filter-chip:last-child,
|
||||
.filter-group-toggle:last-child {
|
||||
border-radius: $es-radius-sm;
|
||||
}
|
||||
|
||||
// Group toggle active/has-selection states propagate to wrapper border
|
||||
.filter-group-toggle.active + .chip-preview-btn {
|
||||
border-left-color: $es-primary;
|
||||
background: rgba($es-primary, 0.05);
|
||||
}
|
||||
|
||||
.filter-group-toggle.has-selection + .chip-preview-btn {
|
||||
border-left-color: $es-success;
|
||||
background: rgba($es-success, 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown content
|
||||
.dropdown-content {
|
||||
max-height: 400px;
|
||||
@@ -1787,7 +1751,7 @@ body > .target-search-dropdown,
|
||||
|
||||
// Results container
|
||||
.dropdown-results {
|
||||
padding: 0 $es-spacing-sm;
|
||||
padding: 0;
|
||||
background: $es-white;
|
||||
min-height: 200px;
|
||||
}
|
||||
@@ -1798,7 +1762,7 @@ body > .target-search-dropdown,
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm 0;
|
||||
padding: $es-spacing-sm;
|
||||
background: $es-white;
|
||||
border: none;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
@@ -2295,15 +2259,15 @@ body > .target-search-dropdown,
|
||||
|
||||
// ============================================================================
|
||||
// Standalone dropdown styles (for when dropdown is appended to body)
|
||||
// These selectors work because .target-search-dropdown is on the dropdown itself
|
||||
// These selectors work because .es-search-dropdown is on the dropdown itself
|
||||
// ============================================================================
|
||||
|
||||
.target-search-dropdown {
|
||||
.es-search-dropdown {
|
||||
// Results container - scrollable
|
||||
.dropdown-results {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 0 $es-spacing-sm;
|
||||
padding: 0;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
@@ -2385,7 +2349,7 @@ body > .target-search-dropdown,
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: 0;
|
||||
padding: $es-spacing-sm;
|
||||
border: none;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
border-radius: 0;
|
||||
@@ -2559,7 +2523,7 @@ body > .target-search-dropdown,
|
||||
}
|
||||
|
||||
// Body-level dropdown (when appended to body for z-index)
|
||||
body > .target-search-dropdown {
|
||||
body > .es-search-dropdown {
|
||||
// Override dropdown-item border when inside body-appended dropdown
|
||||
.dropdown-item {
|
||||
border: none;
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
// Main wrapper (supports both .target-conditions-trait and .entity-selector-trait)
|
||||
.target-conditions-trait,
|
||||
// Main wrapper (supports both .entity-selector-trait and .entity-selector-trait)
|
||||
.entity-selector-trait {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
@@ -174,7 +173,7 @@
|
||||
}
|
||||
|
||||
// Block type tabs
|
||||
.target-block-tabs {
|
||||
.es-block-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
@@ -183,7 +182,7 @@
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
}
|
||||
|
||||
.target-block-tab {
|
||||
.es-block-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -237,7 +236,7 @@
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-lg $es-radius-lg 0 0;
|
||||
|
||||
.target-block-tabs {
|
||||
.es-block-tabs {
|
||||
flex: 1;
|
||||
border-bottom: 0;
|
||||
border-radius: $es-radius-lg 0 0 0;
|
||||
@@ -261,7 +260,7 @@
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
> i {
|
||||
.material-icons {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -279,7 +278,7 @@
|
||||
}
|
||||
|
||||
// Block container
|
||||
.target-block-container {
|
||||
.es-block-container {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
@@ -287,18 +286,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.target-block-content {
|
||||
.es-block-content {
|
||||
padding: $es-spacing-md;
|
||||
}
|
||||
|
||||
.target-block-groups {
|
||||
.es-block-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $es-spacing-md;
|
||||
}
|
||||
|
||||
// Block header (for standalone blocks)
|
||||
.target-block-header {
|
||||
.es-block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -308,7 +307,7 @@
|
||||
}
|
||||
|
||||
// Empty state
|
||||
.target-block-empty {
|
||||
.es-block-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -366,25 +365,18 @@
|
||||
}
|
||||
|
||||
// Single mode specific styles
|
||||
.target-conditions-trait.single-mode,
|
||||
.entity-selector-trait.single-mode,
|
||||
.entity-selector-trait.single-mode {
|
||||
// Hide tabs in standalone layout (has separate header, 1 tab is redundant)
|
||||
.target-block-tabs {
|
||||
.es-block-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.target-block-container {
|
||||
.es-block-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// In form-content layout, always show tabs — they serve as the block title
|
||||
.entity-selector-tabs-row .target-block-tabs {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Header action buttons
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Group container
|
||||
.target-group {
|
||||
.es-group {
|
||||
background: $es-white;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-lg;
|
||||
@@ -18,7 +17,7 @@
|
||||
}
|
||||
|
||||
// Group header
|
||||
.target-group-header {
|
||||
.es-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -28,7 +27,7 @@
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
}
|
||||
|
||||
.target-group-title {
|
||||
.es-group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
@@ -51,7 +50,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.target-group-actions {
|
||||
.es-group-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-xs;
|
||||
@@ -80,7 +79,7 @@
|
||||
}
|
||||
|
||||
// Group body
|
||||
.target-group-body,
|
||||
.es-group-body,
|
||||
.group-body {
|
||||
padding: $es-spacing-md;
|
||||
}
|
||||
@@ -931,36 +930,6 @@
|
||||
background-size: 1.25em 1.25em;
|
||||
}
|
||||
|
||||
// Single mode — strip padding, borders, backgrounds for clean single-selection UI
|
||||
&[data-mode=single],
|
||||
.mode-single {
|
||||
.groups-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.group-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.group-include {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.selection-group {
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Condition match count badge
|
||||
.condition-match-count {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
// Preview Popover Container
|
||||
// =============================================================================
|
||||
|
||||
.target-preview-popover,
|
||||
.target-list-preview-popover {
|
||||
.es-preview-popover,
|
||||
.es-list-preview-popover {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
min-width: 320px;
|
||||
@@ -66,23 +66,6 @@
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Positioned above trigger - arrow pointing down
|
||||
&.position-above {
|
||||
&::before {
|
||||
top: auto;
|
||||
bottom: -8px;
|
||||
border-top: 8px solid $es-border-color;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: auto;
|
||||
bottom: -6px;
|
||||
border-top: 6px solid $es-white;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -579,6 +562,7 @@
|
||||
}
|
||||
|
||||
// Icon styles
|
||||
> .material-icons,
|
||||
> i:first-child {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
@@ -638,7 +622,7 @@
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
> i {
|
||||
.material-icons {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Method dropdown trigger button
|
||||
@@ -14,8 +13,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 36px;
|
||||
padding: 0 $es-spacing-md;
|
||||
min-height: 36px;
|
||||
padding: 0.25rem $es-spacing-md;
|
||||
border-radius: $es-radius-md;
|
||||
background: $es-white;
|
||||
color: $es-slate-800;
|
||||
@@ -53,6 +52,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: $es-font-weight-medium;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.method-trigger-caret {
|
||||
|
||||
@@ -1,488 +1,2 @@
|
||||
/**
|
||||
* Modal Component
|
||||
* Preview modals, confirmation dialogs
|
||||
*/
|
||||
|
||||
@use "sass:color";
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
// Modal backdrop
|
||||
.mpr-modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: $es-z-modal;
|
||||
opacity: 0;
|
||||
transition: opacity $es-transition-normal;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal container (exclude Bootstrap .modal to prevent collision)
|
||||
.mpr-modal:not(.modal) {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.95);
|
||||
z-index: $es-z-modal + 1;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
background: $es-white;
|
||||
border-radius: $es-radius-xl;
|
||||
box-shadow: $es-shadow-xl;
|
||||
opacity: 0;
|
||||
transition: all $es-transition-normal;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
&.modal-sm {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
&.modal-lg {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&.modal-xl {
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
&.modal-fullscreen {
|
||||
width: 95%;
|
||||
max-width: none;
|
||||
height: 90vh;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal header
|
||||
.mpr-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $es-spacing-md;
|
||||
padding: $es-spacing-md $es-spacing-lg;
|
||||
background: $es-bg-header;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mpr-modal-title {
|
||||
font-size: $es-font-size-base;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mpr-modal-close {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: $es-text-muted;
|
||||
border-radius: $es-radius-md;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: $es-font-size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal body
|
||||
.mpr-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $es-spacing-lg;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
// Modal footer
|
||||
.mpr-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-md $es-spacing-lg;
|
||||
background: $es-bg-header;
|
||||
border-top: 1px solid $es-border-color;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mpr-modal-btn {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $es-spacing-xs;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-medium;
|
||||
border-radius: $es-radius-md;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&.btn-secondary {
|
||||
color: $es-text-secondary;
|
||||
background: $es-slate-100;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
color: $es-white;
|
||||
background: $es-primary;
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
color: $es-white;
|
||||
background: $es-danger;
|
||||
|
||||
&:hover {
|
||||
background: color.adjust($es-danger, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Preview popover styles moved to _list-preview.scss
|
||||
|
||||
.popover-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-bg-header;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-lg $es-radius-lg 0 0;
|
||||
}
|
||||
|
||||
.popover-title {
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
}
|
||||
|
||||
.popover-close {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: $es-text-muted;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: $es-spacing-sm;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
.popover-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-bg-header;
|
||||
border-top: 1px solid $es-border-color;
|
||||
border-radius: 0 0 $es-radius-lg $es-radius-lg;
|
||||
}
|
||||
|
||||
.popover-info {
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
|
||||
.popover-load-more {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-medium;
|
||||
color: $es-primary;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Popover arrow
|
||||
.popover-arrow {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: $es-white;
|
||||
border: 1px solid $es-border-color;
|
||||
transform: rotate(45deg);
|
||||
|
||||
&.arrow-top {
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.arrow-bottom {
|
||||
bottom: -7px;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Holiday Preview Modal
|
||||
// ==========================================================================
|
||||
|
||||
#mpr-holiday-preview-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $es-z-modal;
|
||||
|
||||
&.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mpr-modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mpr-modal-dialog {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
max-height: 80vh;
|
||||
background: $es-white;
|
||||
border-radius: $es-radius-lg;
|
||||
box-shadow: $es-shadow-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mpr-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $es-spacing-md;
|
||||
padding: $es-spacing-md $es-spacing-lg;
|
||||
background: $es-bg-header;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mpr-modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
font-size: $es-font-size-base;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
margin: 0;
|
||||
|
||||
i> i {
|
||||
font-size: 20px;
|
||||
color: $es-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.mpr-modal-close {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: $es-text-muted;
|
||||
border-radius: $es-radius-md;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.mpr-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $es-spacing-lg;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
.holiday-preview-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-xl 0;
|
||||
color: $es-text-muted;
|
||||
font-size: $es-font-size-sm;
|
||||
|
||||
i {
|
||||
font-size: $es-font-size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
.holiday-preview-empty {
|
||||
text-align: center;
|
||||
padding: $es-spacing-xl 0;
|
||||
color: $es-text-muted;
|
||||
|
||||
i> i {
|
||||
font-size: 48px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: $es-spacing-md;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $es-spacing-xs;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// Holiday list
|
||||
.holiday-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $es-spacing-sm;
|
||||
}
|
||||
|
||||
.holiday-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $es-spacing-md;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-slate-50;
|
||||
border-radius: $es-radius-md;
|
||||
border-left: 3px solid $es-success;
|
||||
|
||||
&.holiday-type-bank {
|
||||
border-left-color: $es-info;
|
||||
}
|
||||
|
||||
&.holiday-type-observance {
|
||||
border-left-color: $es-warning;
|
||||
}
|
||||
|
||||
&.holiday-type-regional {
|
||||
border-left-color: #8b5cf6;
|
||||
}
|
||||
}
|
||||
|
||||
.holiday-date {
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
|
||||
.holiday-day {
|
||||
display: block;
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
}
|
||||
|
||||
.holiday-weekday {
|
||||
display: block;
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.holiday-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.holiday-name {
|
||||
display: block;
|
||||
font-size: $es-font-size-sm;
|
||||
color: $es-text-primary;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.holiday-type-badge {
|
||||
display: inline-block;
|
||||
margin-top: $es-spacing-xs;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 10px;
|
||||
font-weight: $es-font-weight-medium;
|
||||
text-transform: capitalize;
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
border-radius: $es-radius-sm;
|
||||
}
|
||||
|
||||
.holiday-preview-note {
|
||||
margin-top: $es-spacing-md;
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
// Modal component removed - was dead code conflicting with Bootstrap .modal
|
||||
// Modal functionality is provided by prestashop-admin package (mpr-admin-modal-* classes)
|
||||
|
||||
76
sources/scss/components/_replace-modal.scss
Normal file
76
sources/scss/components/_replace-modal.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
// Replace confirmation modal for single-mode entity selectors
|
||||
// Displayed when user tries to replace an already-selected item
|
||||
|
||||
@use '../variables' as *;
|
||||
|
||||
.mpr-replace-body {
|
||||
padding: $es-spacing-sm 0;
|
||||
}
|
||||
|
||||
.mpr-replace-message {
|
||||
margin: 0 0 $es-spacing-md;
|
||||
color: $es-text-secondary;
|
||||
font-size: $es-font-size-sm;
|
||||
line-height: $es-line-height-normal;
|
||||
}
|
||||
|
||||
.mpr-replace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
border-radius: $es-radius-md;
|
||||
border: 1px solid $es-border-color;
|
||||
background: $es-white;
|
||||
}
|
||||
|
||||
.mpr-replace-current {
|
||||
border-color: $es-danger;
|
||||
background: $es-danger-light;
|
||||
|
||||
.mpr-replace-label {
|
||||
color: $es-danger-dark;
|
||||
background: rgba($es-danger, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.mpr-replace-new {
|
||||
border-color: $es-success;
|
||||
background: $es-success-light;
|
||||
|
||||
.mpr-replace-label {
|
||||
color: $es-success-dark;
|
||||
background: rgba($es-success, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.mpr-replace-label {
|
||||
flex-shrink: 0;
|
||||
padding: 2px $es-spacing-sm;
|
||||
border-radius: $es-radius-sm;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.mpr-replace-value {
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-medium;
|
||||
color: $es-text-primary;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mpr-replace-arrow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: $es-spacing-xs 0;
|
||||
color: $es-text-light;
|
||||
|
||||
.material-icons {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@
|
||||
background: $es-slate-200;
|
||||
}
|
||||
|
||||
> i {
|
||||
.material-icons {
|
||||
color: $es-slate-400;
|
||||
font-size: 20px;
|
||||
}
|
||||
@@ -357,7 +357,7 @@
|
||||
border-radius: $es-radius-full;
|
||||
white-space: nowrap;
|
||||
|
||||
> i {
|
||||
.material-icons {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Tips box container
|
||||
.target-tips-box {
|
||||
.es-tips-box {
|
||||
margin: $es-spacing-lg $es-spacing-md $es-spacing-md;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-lg;
|
||||
@@ -55,7 +54,7 @@
|
||||
}
|
||||
|
||||
// Expanded state
|
||||
.target-tips-box.expanded {
|
||||
.es-tips-box.expanded {
|
||||
.tips-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
vertical-align: middle;
|
||||
margin-left: 0.25rem;
|
||||
|
||||
> i {
|
||||
font-size: 14px;
|
||||
.material-icons {
|
||||
font-size: 16px;
|
||||
color: $es-text-muted;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover > i {
|
||||
&:hover .material-icons {
|
||||
color: $es-primary;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@
|
||||
line-height: 1;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
> i {
|
||||
.material-icons {
|
||||
font-size: 16px;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
@@ -100,7 +100,7 @@
|
||||
&:hover {
|
||||
background: $es-slate-100;
|
||||
|
||||
> i {
|
||||
.material-icons {
|
||||
color: $es-slate-700;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
}
|
||||
|
||||
// Tree view mode in dropdown
|
||||
.target-search-dropdown.view-tree {
|
||||
.es-search-dropdown.view-tree {
|
||||
.dropdown-results {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Value picker container
|
||||
|
||||
@@ -6,14 +6,12 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
// Base border reset for all entity-selector elements
|
||||
.target-conditions-trait,
|
||||
.target-conditions-trait *,
|
||||
.entity-selector-trait,
|
||||
.entity-selector-trait *,
|
||||
.method-dropdown-menu,
|
||||
.method-dropdown-menu *,
|
||||
.target-preview-popover,
|
||||
.target-preview-popover * {
|
||||
.es-preview-popover,
|
||||
.es-preview-popover * {
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
border-color: $es-border-color;
|
||||
@@ -22,7 +20,7 @@
|
||||
// Full-width form group override using :has()
|
||||
// Excludes .layout-form-group which uses standard PrestaShop form layout
|
||||
.form-group:has(.entity-selector-trait:not(.layout-form-group)),
|
||||
.form-group:has(.target-conditions-trait:not(.layout-form-group)),
|
||||
.form-group:has(.entity-selector-trait:not(.layout-form-group)),
|
||||
.form-group:has(.condition-trait:not(.layout-form-group)) {
|
||||
display: block;
|
||||
|
||||
@@ -65,26 +63,26 @@
|
||||
|
||||
// Dropdown overflow fix
|
||||
// When dropdown is open, parent containers must allow overflow
|
||||
.panel:has(.target-search-dropdown.show),
|
||||
.card:has(.target-search-dropdown.show),
|
||||
.form-wrapper:has(.target-search-dropdown.show),
|
||||
.panel-body:has(.target-search-dropdown.show),
|
||||
.card-body:has(.target-search-dropdown.show),
|
||||
.form-group:has(.target-search-dropdown.show),
|
||||
.col-lg-8:has(.target-search-dropdown.show),
|
||||
.col-lg-12:has(.target-search-dropdown.show) {
|
||||
.panel:has(.es-search-dropdown.show),
|
||||
.card:has(.es-search-dropdown.show),
|
||||
.form-wrapper:has(.es-search-dropdown.show),
|
||||
.panel-body:has(.es-search-dropdown.show),
|
||||
.card-body:has(.es-search-dropdown.show),
|
||||
.form-group:has(.es-search-dropdown.show),
|
||||
.col-lg-8:has(.es-search-dropdown.show),
|
||||
.col-lg-12:has(.es-search-dropdown.show) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
// Target conditions wrapper hierarchy overflow fix
|
||||
.target-conditions-trait:has(.target-search-dropdown.show),
|
||||
.entity-selector-trait:has(.target-search-dropdown.show),
|
||||
.condition-trait-body:has(.target-search-dropdown.show),
|
||||
.target-block-content:has(.target-search-dropdown.show),
|
||||
.target-block-groups:has(.target-search-dropdown.show),
|
||||
.target-group:has(.target-search-dropdown.show),
|
||||
.target-group-body:has(.target-search-dropdown.show),
|
||||
.target-search-wrapper:has(.target-search-dropdown.show) {
|
||||
.entity-selector-trait:has(.es-search-dropdown.show),
|
||||
.entity-selector-trait:has(.es-search-dropdown.show),
|
||||
.condition-trait-body:has(.es-search-dropdown.show),
|
||||
.es-block-content:has(.es-search-dropdown.show),
|
||||
.es-block-groups:has(.es-search-dropdown.show),
|
||||
.es-group:has(.es-search-dropdown.show),
|
||||
.es-group-body:has(.es-search-dropdown.show),
|
||||
.es-search-wrapper:has(.es-search-dropdown.show) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@@ -94,7 +92,7 @@
|
||||
// Use .layout-embedded for entity selectors nested inside other components
|
||||
// Removes outer wrapper styling to avoid redundant borders/backgrounds
|
||||
|
||||
.target-conditions-trait.layout-embedded,
|
||||
.entity-selector-trait.layout-embedded,
|
||||
.entity-selector-trait.layout-embedded {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
// Tablet and below
|
||||
@media (max-width: 991px) {
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
.condition-trait-header {
|
||||
flex-direction: column;
|
||||
@@ -20,7 +19,7 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.target-block-tabs {
|
||||
.es-block-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -28,19 +27,18 @@
|
||||
|
||||
// Mobile
|
||||
@media (max-width: 767px) {
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
.target-block-tab {
|
||||
.es-block-tab {
|
||||
padding: $es-spacing-sm;
|
||||
font-size: $es-font-size-xs;
|
||||
}
|
||||
|
||||
.target-group-header {
|
||||
.es-group-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.target-search-dropdown {
|
||||
.es-search-dropdown {
|
||||
width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
@@ -54,7 +52,6 @@
|
||||
|
||||
// High-resolution displays
|
||||
@media (min-width: 1600px) {
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
.dropdown-results-grid.view-grid-3 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
@@ -31,3 +31,44 @@
|
||||
@use 'components/tooltip';
|
||||
@use 'components/tree';
|
||||
@use 'components/validation';
|
||||
@use 'components/replace-modal';
|
||||
|
||||
// Refactor additions (Mar 2026)
|
||||
// Loading state
|
||||
.loading-count { opacity: 0.5; transition: opacity 0.15s; }
|
||||
.tab-badge.loading { opacity: 0.5; transition: opacity 0.15s; }
|
||||
|
||||
// Expand/collapse CSS transitions (replaces jQuery slideDown/slideUp)
|
||||
.entity-selector-blocks-content,
|
||||
.condition-trait-body,
|
||||
.group-modifiers-content {
|
||||
transition: max-height 0.2s ease-out, opacity 0.2s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.entity-selector-blocks-content:not(.es-expanded),
|
||||
.condition-trait-body:not(.es-expanded),
|
||||
.group-modifiers-content:not(.es-expanded) {
|
||||
max-height: 0 !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.entity-selector-blocks-content.es-expanded,
|
||||
.condition-trait-body.es-expanded,
|
||||
.group-modifiers-content.es-expanded {
|
||||
max-height: 2000px;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// Empty state component
|
||||
.es-empty-state,
|
||||
.chips-empty-state {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
// chips-wrapper, chips-toolbar, and chips-load-more are created by JS
|
||||
// only when chips exist — not rendered in template when empty
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ use Configuration;
|
||||
use Tools;
|
||||
use ImageType;
|
||||
use Shop;
|
||||
use MyPrestaRocks\Search\SearchEngine;
|
||||
|
||||
class EntitySearchEngine
|
||||
{
|
||||
@@ -34,6 +35,16 @@ class EntitySearchEngine
|
||||
*/
|
||||
protected $idShop;
|
||||
|
||||
/**
|
||||
* @var \MyPrestaRocks\Search\Tokenizer|null Lazy-loaded tokenizer
|
||||
*/
|
||||
protected $tokenizer;
|
||||
|
||||
/**
|
||||
* @var \MyPrestaRocks\Search\WordVariants|null Lazy-loaded word variants
|
||||
*/
|
||||
protected $wordVariants;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -142,6 +153,99 @@ class EntitySearchEngine
|
||||
return str_replace(['%', '_'], ['\\%', '\\_'], pSQL($pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build smart LIKE conditions for a search query against one or more columns.
|
||||
* Uses WordVariants for stemming so "shower" also matches "showers", etc.
|
||||
* Falls back to simple LIKE if prestashop-search is not available.
|
||||
*
|
||||
* @param string $query Search query
|
||||
* @param string[] $columns SQL columns to match (e.g. ['pl.name', 'p.reference'])
|
||||
* @return string SQL WHERE fragment (without leading AND/OR), or empty string
|
||||
*/
|
||||
protected function buildSmartSearch($query, array $columns)
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query === '' || empty($columns)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!class_exists('MyPrestaRocks\\Search\\Tokenizer')) {
|
||||
// Fallback: simple LIKE
|
||||
$escaped = $this->escapePattern($query);
|
||||
$likes = [];
|
||||
foreach ($columns as $col) {
|
||||
$likes[] = $col . " LIKE '%" . $escaped . "%'";
|
||||
}
|
||||
return '(' . implode(' OR ', $likes) . ')';
|
||||
}
|
||||
|
||||
if ($this->tokenizer === null) {
|
||||
$this->tokenizer = new \MyPrestaRocks\Search\Tokenizer();
|
||||
$langIso = Context::getContext()->language->iso_code ?? null;
|
||||
$this->wordVariants = new \MyPrestaRocks\Search\WordVariants($langIso);
|
||||
}
|
||||
|
||||
$parsed = $this->tokenizer->parse($query);
|
||||
$words = $parsed['words'];
|
||||
$dimensions = isset($parsed['dimensions']) ? $parsed['dimensions'] : [];
|
||||
|
||||
if (empty($words) && empty($dimensions)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$wordGroups = [];
|
||||
|
||||
// Regular word groups (AND between words, OR between variants)
|
||||
foreach ($words as $word) {
|
||||
if (mb_strlen($word) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variants = $this->wordVariants->getVariants($word);
|
||||
$variants = array_unique($variants);
|
||||
|
||||
$variantConditions = [];
|
||||
foreach ($variants as $variant) {
|
||||
$escaped = pSQL($variant);
|
||||
foreach ($columns as $col) {
|
||||
$variantConditions[] = $col . " LIKE '%" . $escaped . "%'";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($variantConditions)) {
|
||||
$wordGroups[] = '(' . implode(' OR ', $variantConditions) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// Dimension groups — generate LIKE conditions for all variants
|
||||
if (!empty($dimensions) && class_exists('MyPrestaRocks\\Search\\DimensionHandler')) {
|
||||
$dimHandler = new \MyPrestaRocks\Search\DimensionHandler();
|
||||
foreach ($dimensions as $dim) {
|
||||
$dimVariants = $dimHandler->getVariants($dim);
|
||||
$dimConditions = [];
|
||||
foreach ($dimVariants as $variant => $quality) {
|
||||
if ($quality < 0.3) {
|
||||
continue;
|
||||
}
|
||||
$escaped = pSQL($variant);
|
||||
foreach ($columns as $col) {
|
||||
$dimConditions[] = $col . " LIKE '%" . $escaped . "%'";
|
||||
}
|
||||
}
|
||||
if (!empty($dimConditions)) {
|
||||
$wordGroups[] = '(' . implode(' OR ', $dimConditions) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($wordGroups)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// All groups must match (AND between word groups and dimension groups)
|
||||
return '(' . implode(' AND ', $wordGroups) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ORDER BY clause based on entity type and sort field
|
||||
*
|
||||
@@ -207,10 +311,32 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('stock_available', 'sa', 'sa.id_product = p.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . (int) $idShop);
|
||||
$sql->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
|
||||
|
||||
// Search query
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
|
||||
// Search query — use shared SearchEngine when available, fall back to LIKE
|
||||
$compiled = null;
|
||||
$hasSearchEngine = class_exists('MyPrestaRocks\\Search\\SearchEngine');
|
||||
if (!empty($query) && $hasSearchEngine) {
|
||||
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
|
||||
'fuzzy_enabled' => true,
|
||||
'variants_enabled' => true,
|
||||
'search_attributes' => false,
|
||||
'search_features' => false,
|
||||
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
|
||||
]);
|
||||
$compiled = $searchEngine->compile($query);
|
||||
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
|
||||
if (!empty($whereClause)) {
|
||||
$sql->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
|
||||
$sql->select('(' . $compiled['score'] . ') AS relevance_score');
|
||||
} else {
|
||||
$sql->where('p.id_product = ' . (int) $query);
|
||||
$sql->select('0 AS relevance_score');
|
||||
}
|
||||
} elseif (!empty($query)) {
|
||||
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
|
||||
$sql->select('0 AS relevance_score');
|
||||
} else {
|
||||
$sql->select('0 AS relevance_score');
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
@@ -235,8 +361,14 @@ class EntitySearchEngine
|
||||
'reference' => 'p.reference',
|
||||
'popularity' => 'sales_qty',
|
||||
'stock' => 'stock_qty',
|
||||
'relevance' => 'relevance_score',
|
||||
];
|
||||
$sql->orderBy($this->buildOrderBy('products', $filters, $productSortMap));
|
||||
// Default to relevance sort when searching
|
||||
if (!empty($query) && empty($filters['sort_by'])) {
|
||||
$sql->orderBy('relevance_score DESC, pl.name ASC');
|
||||
} else {
|
||||
$sql->orderBy($this->buildOrderBy('products', $filters, $productSortMap));
|
||||
}
|
||||
$sql->limit((int) $limit, (int) $offset);
|
||||
|
||||
$results = Db::getInstance()->executeS($sql);
|
||||
@@ -313,9 +445,24 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
|
||||
$sql->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
|
||||
if (!empty($query) && class_exists('MyPrestaRocks\\Search\\SearchEngine')) {
|
||||
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
|
||||
'fuzzy_enabled' => true,
|
||||
'variants_enabled' => true,
|
||||
'search_attributes' => false,
|
||||
'search_features' => false,
|
||||
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
|
||||
]);
|
||||
$compiled = $searchEngine->compile($query);
|
||||
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
|
||||
if (!empty($whereClause)) {
|
||||
$sql->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
|
||||
} else {
|
||||
$sql->where('p.id_product = ' . (int) $query);
|
||||
}
|
||||
} elseif (!empty($query)) {
|
||||
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
$this->applyProductFilters($sql, $filters, $idLang, $idShop);
|
||||
@@ -450,7 +597,6 @@ class EntitySearchEngine
|
||||
public function searchProductCombinations($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
|
||||
{
|
||||
$db = Db::getInstance();
|
||||
$escapedQuery = !empty($query) ? $this->escapePattern($query) : '';
|
||||
|
||||
// Step 1: Find matching products (same query as searchTargetProducts but no limit — we paginate the flat result)
|
||||
$sqlProducts = new DbQuery();
|
||||
@@ -469,12 +615,29 @@ class EntitySearchEngine
|
||||
$sqlProducts->leftJoin('stock_available', 'sa', 'sa.id_product = p.id_product AND sa.id_product_attribute = 0 AND sa.id_shop = ' . (int) $idShop);
|
||||
$sqlProducts->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
|
||||
|
||||
if (!empty($escapedQuery)) {
|
||||
$sqlProducts->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
|
||||
if (!empty($query) && class_exists('MyPrestaRocks\\Search\\SearchEngine')) {
|
||||
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
|
||||
'fuzzy_enabled' => true,
|
||||
'variants_enabled' => true,
|
||||
'search_attributes' => false,
|
||||
'search_features' => false,
|
||||
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
|
||||
]);
|
||||
$compiled = $searchEngine->compile($query);
|
||||
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
|
||||
if (!empty($whereClause)) {
|
||||
$sqlProducts->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
|
||||
$sqlProducts->select('(' . $compiled['score'] . ') AS relevance_score');
|
||||
} else {
|
||||
$sqlProducts->where('p.id_product = ' . (int) $query);
|
||||
}
|
||||
} elseif (!empty($query)) {
|
||||
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
|
||||
$sqlProducts->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
$this->applyProductFilters($sqlProducts, $filters, $idLang, $idShop);
|
||||
$sqlProducts->orderBy('pl.name ASC');
|
||||
$sqlProducts->orderBy(!empty($query) && !empty($compiled) ? 'relevance_score DESC, pl.name ASC' : 'pl.name ASC');
|
||||
|
||||
$matchingProducts = $db->executeS($sqlProducts);
|
||||
if (!$matchingProducts) {
|
||||
@@ -686,7 +849,6 @@ class EntitySearchEngine
|
||||
public function countProductCombinations($query, $idLang, $idShop, array $filters = [])
|
||||
{
|
||||
$db = Db::getInstance();
|
||||
$escapedQuery = !empty($query) ? $this->escapePattern($query) : '';
|
||||
|
||||
// Get matching product IDs
|
||||
$sqlProducts = new DbQuery();
|
||||
@@ -695,8 +857,24 @@ class EntitySearchEngine
|
||||
$sqlProducts->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
|
||||
$sqlProducts->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($escapedQuery)) {
|
||||
$sqlProducts->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.reference LIKE \'%' . $escapedQuery . '%\' OR p.id_product = ' . (int) $query . ')');
|
||||
if (!empty($query) && class_exists('MyPrestaRocks\\Search\\SearchEngine')) {
|
||||
$searchEngine = new SearchEngine((int) $idLang, (int) $idShop, [
|
||||
'fuzzy_enabled' => true,
|
||||
'variants_enabled' => true,
|
||||
'search_attributes' => false,
|
||||
'search_features' => false,
|
||||
'weights' => ['name' => 10, 'reference' => 8, 'description_short' => 0, 'description' => 0, 'ean13' => 5],
|
||||
]);
|
||||
$compiled = $searchEngine->compile($query);
|
||||
$whereClause = $searchEngine->buildWhereFromConditions($compiled['conditions']);
|
||||
if (!empty($whereClause)) {
|
||||
$sqlProducts->where('((' . $whereClause . ') OR p.id_product = ' . (int) $query . ')');
|
||||
} else {
|
||||
$sqlProducts->where('p.id_product = ' . (int) $query);
|
||||
}
|
||||
} elseif (!empty($query)) {
|
||||
$smartWhere = $this->buildSmartSearch($query, ['pl.name', 'p.reference']);
|
||||
$sqlProducts->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_product = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
$this->applyProductFilters($sqlProducts, $filters, $idLang, $idShop);
|
||||
@@ -1080,8 +1258,8 @@ class EntitySearchEngine
|
||||
$sql->where('c.id_parent > 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.id_category = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_category = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
// Refine query
|
||||
@@ -1174,8 +1352,8 @@ class EntitySearchEngine
|
||||
$sql->where('c.id_parent > 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.id_category = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_category = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1429,8 +1607,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('manufacturer_shop', 'ms', 'ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(m.name LIKE \'%' . $escapedQuery . '%\' OR m.id_manufacturer = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['m.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR m.id_manufacturer = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1499,8 +1677,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('manufacturer_shop', 'ms', 'ms.id_manufacturer = m.id_manufacturer AND ms.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(m.name LIKE \'%' . $escapedQuery . '%\' OR m.id_manufacturer = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['m.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR m.id_manufacturer = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1577,8 +1755,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('supplier_shop', 'ss', 'ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_supplier = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_supplier = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1631,8 +1809,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('supplier_shop', 'ss', 'ss.id_supplier = s.id_supplier AND ss.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_supplier = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_supplier = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1709,8 +1887,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('cms_category_lang', 'ccl', 'ccl.id_cms_category = c.id_cms_category AND ccl.id_lang = ' . (int) $idLang . ' AND ccl.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.meta_title LIKE \'%' . $escapedQuery . '%\' OR c.id_cms = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.meta_title']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_cms = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1765,8 +1943,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('cms_lang', 'cl', 'cl.id_cms = c.id_cms AND cl.id_lang = ' . (int) $idLang . ' AND cl.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.meta_title LIKE \'%' . $escapedQuery . '%\' OR c.id_cms = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.meta_title']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_cms = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1845,8 +2023,8 @@ class EntitySearchEngine
|
||||
$sql->where('cc.id_parent > 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(ccl.name LIKE \'%' . $escapedQuery . '%\' OR cc.id_cms_category = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['ccl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR cc.id_cms_category = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1902,8 +2080,8 @@ class EntitySearchEngine
|
||||
$sql->where('cc.id_parent > 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(ccl.name LIKE \'%' . $escapedQuery . '%\' OR cc.id_cms_category = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['ccl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR cc.id_cms_category = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -1981,8 +2159,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = e.id_profile AND pl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(e.firstname LIKE \'%' . $escapedQuery . '%\' OR e.lastname LIKE \'%' . $escapedQuery . '%\' OR e.email LIKE \'%' . $escapedQuery . '%\' OR e.id_employee = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['e.firstname', 'e.lastname', 'e.email']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR e.id_employee = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2034,8 +2212,8 @@ class EntitySearchEngine
|
||||
$sql->from('employee', 'e');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(e.firstname LIKE \'%' . $escapedQuery . '%\' OR e.lastname LIKE \'%' . $escapedQuery . '%\' OR e.email LIKE \'%' . $escapedQuery . '%\' OR e.id_employee = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['e.firstname', 'e.lastname', 'e.email']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR e.id_employee = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2113,8 +2291,8 @@ class EntitySearchEngine
|
||||
$sql->where('c.deleted = 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(c.firstname LIKE \'%' . $escapedQuery . '%\' OR c.lastname LIKE \'%' . $escapedQuery . '%\' OR c.email LIKE \'%' . $escapedQuery . '%\' OR c.company LIKE \'%' . $escapedQuery . '%\' OR c.id_customer = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['c.firstname', 'c.lastname', 'c.email', 'c.company']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_customer = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2171,8 +2349,8 @@ class EntitySearchEngine
|
||||
$sql->where('c.deleted = 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(c.firstname LIKE \'%' . $escapedQuery . '%\' OR c.lastname LIKE \'%' . $escapedQuery . '%\' OR c.email LIKE \'%' . $escapedQuery . '%\' OR c.company LIKE \'%' . $escapedQuery . '%\' OR c.id_customer = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['c.firstname', 'c.lastname', 'c.email', 'c.company']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_customer = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2251,8 +2429,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(gl.name LIKE \'%' . $escapedQuery . '%\' OR g.id_group = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['gl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR g.id_group = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2303,8 +2481,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('group_lang', 'gl', 'gl.id_group = g.id_group AND gl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(gl.name LIKE \'%' . $escapedQuery . '%\' OR g.id_group = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['gl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR g.id_group = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2376,8 +2554,8 @@ class EntitySearchEngine
|
||||
$sql->where('c.deleted = 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(c.name LIKE \'%' . $escapedQuery . '%\' OR c.id_carrier = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['c.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_carrier = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2432,8 +2610,8 @@ class EntitySearchEngine
|
||||
$sql->where('c.deleted = 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(c.name LIKE \'%' . $escapedQuery . '%\' OR c.id_carrier = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['c.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_carrier = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2509,8 +2687,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('zone_shop', 'zs', 'zs.id_zone = z.id_zone AND zs.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(z.name LIKE \'%' . $escapedQuery . '%\' OR z.id_zone = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['z.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR z.id_zone = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2561,8 +2739,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('zone_shop', 'zs', 'zs.id_zone = z.id_zone AND zs.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(z.name LIKE \'%' . $escapedQuery . '%\' OR z.id_zone = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['z.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR z.id_zone = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2640,8 +2818,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('zone', 'z', 'z.id_zone = c.id_zone');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_country = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_country = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2722,8 +2900,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('country_lang', 'cl', 'cl.id_country = c.id_country AND cl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_country = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_country = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2824,8 +3002,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_currency = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_currency = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2882,8 +3060,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('currency_lang', 'cl', 'cl.id_currency = c.id_currency AND cl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(cl.name LIKE \'%' . $escapedQuery . '%\' OR c.iso_code LIKE \'%' . $escapedQuery . '%\' OR c.id_currency = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['cl.name', 'c.iso_code']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR c.id_currency = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -2962,8 +3140,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('lang_shop', 'ls', 'ls.id_lang = l.id_lang AND ls.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(l.name LIKE \'%' . $escapedQuery . '%\' OR l.iso_code LIKE \'%' . $escapedQuery . '%\' OR l.id_lang = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['l.name', 'l.iso_code']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR l.id_lang = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3018,8 +3196,8 @@ class EntitySearchEngine
|
||||
$sql->innerJoin('lang_shop', 'ls', 'ls.id_lang = l.id_lang AND ls.id_shop = ' . (int) $idShop);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(l.name LIKE \'%' . $escapedQuery . '%\' OR l.iso_code LIKE \'%' . $escapedQuery . '%\' OR l.id_lang = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['l.name', 'l.iso_code']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR l.id_lang = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3097,8 +3275,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('shop_group', 'sg', 'sg.id_shop_group = s.id_shop_group');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_shop = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_shop = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3150,8 +3328,8 @@ class EntitySearchEngine
|
||||
$sql->from('shop', 's');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(s.name LIKE \'%' . $escapedQuery . '%\' OR s.id_shop = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['s.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR s.id_shop = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3227,8 +3405,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.id_profile = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['pl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_profile = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3274,8 +3452,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('profile_lang', 'pl', 'pl.id_profile = p.id_profile AND pl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(pl.name LIKE \'%' . $escapedQuery . '%\' OR p.id_profile = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['pl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR p.id_profile = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3348,8 +3526,8 @@ class EntitySearchEngine
|
||||
$sql->where('os.deleted = 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(osl.name LIKE \'%' . $escapedQuery . '%\' OR os.id_order_state = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['osl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR os.id_order_state = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3401,8 +3579,8 @@ class EntitySearchEngine
|
||||
$sql->where('os.deleted = 0');
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(osl.name LIKE \'%' . $escapedQuery . '%\' OR os.id_order_state = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['osl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR os.id_order_state = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3475,8 +3653,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(tl.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tax = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['tl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tax = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3529,8 +3707,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('tax_lang', 'tl', 'tl.id_tax = t.id_tax AND tl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(tl.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tax = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['tl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tax = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3610,8 +3788,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(al.name LIKE \'%' . $escapedQuery . '%\' OR agl.name LIKE \'%' . $escapedQuery . '%\' OR a.id_attribute = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['al.name', 'agl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR a.id_attribute = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3677,8 +3855,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(al.name LIKE \'%' . $escapedQuery . '%\' OR agl.name LIKE \'%' . $escapedQuery . '%\' OR a.id_attribute = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['al.name', 'agl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR a.id_attribute = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3762,8 +3940,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(fvl.value LIKE \'%' . $escapedQuery . '%\' OR fl.name LIKE \'%' . $escapedQuery . '%\' OR fv.id_feature_value = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['fvl.value', 'fl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR fv.id_feature_value = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3825,8 +4003,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('feature_lang', 'fl', 'fl.id_feature = fv.id_feature AND fl.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(fvl.value LIKE \'%' . $escapedQuery . '%\' OR fl.name LIKE \'%' . $escapedQuery . '%\' OR fv.id_feature_value = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['fvl.value', 'fl.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR fv.id_feature_value = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3911,8 +4089,8 @@ class EntitySearchEngine
|
||||
$sql->where('t.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(t.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tag = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['t.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tag = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -3960,8 +4138,8 @@ class EntitySearchEngine
|
||||
$sql->where('t.id_lang = ' . (int) $idLang);
|
||||
|
||||
if (!empty($query)) {
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
$sql->where('(t.name LIKE \'%' . $escapedQuery . '%\' OR t.id_tag = ' . (int) $query . ')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['t.name']);
|
||||
$sql->where('(' . ($smartWhere ?: '1=0') . ' OR t.id_tag = ' . (int) $query . ')');
|
||||
}
|
||||
|
||||
if (!empty($filters['refine'])) {
|
||||
@@ -4025,7 +4203,6 @@ class EntitySearchEngine
|
||||
public function searchTargetMprMaterials($query, $idLang, $idShop, $limit = 20, $offset = 0, array $filters = [])
|
||||
{
|
||||
$db = Db::getInstance();
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
|
||||
$sql = new DbQuery();
|
||||
$sql->select('m.id_material, m.reference, m.unit, m.unit_cost, m.active,
|
||||
@@ -4036,7 +4213,8 @@ class EntitySearchEngine
|
||||
$sql->leftJoin('mprwarehouserevolution_material_stock', 'ms', 'ms.id_material = m.id_material');
|
||||
|
||||
if ($query !== '') {
|
||||
$sql->where('(ml.name LIKE \'%' . $escapedQuery . '%\' OR m.reference LIKE \'%' . $escapedQuery . '%\' OR m.barcode LIKE \'%' . $escapedQuery . '%\')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['ml.name', 'm.reference', 'm.barcode']);
|
||||
if ($smartWhere) { $sql->where($smartWhere); }
|
||||
}
|
||||
|
||||
$sql->groupBy('m.id_material');
|
||||
@@ -4073,15 +4251,14 @@ class EntitySearchEngine
|
||||
*/
|
||||
public function countTargetMprMaterials($query, $idLang, $idShop, array $filters = [])
|
||||
{
|
||||
$escapedQuery = $this->escapePattern($query);
|
||||
|
||||
$sql = new DbQuery();
|
||||
$sql->select('COUNT(DISTINCT m.id_material)');
|
||||
$sql->from('mprwarehouserevolution_material', 'm');
|
||||
$sql->leftJoin('mprwarehouserevolution_material_lang', 'ml', 'ml.id_material = m.id_material AND ml.id_lang = ' . (int) $idLang);
|
||||
|
||||
if ($query !== '') {
|
||||
$sql->where('(ml.name LIKE \'%' . $escapedQuery . '%\' OR m.reference LIKE \'%' . $escapedQuery . '%\' OR m.barcode LIKE \'%' . $escapedQuery . '%\')');
|
||||
$smartWhere = $this->buildSmartSearch($query, ['ml.name', 'm.reference', 'm.barcode']);
|
||||
if ($smartWhere) { $sql->where($smartWhere); }
|
||||
}
|
||||
|
||||
return (int) Db::getInstance()->getValue($sql);
|
||||
|
||||
@@ -53,210 +53,6 @@ class EntitySelectorRenderer
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Icon framework abstraction (Material Icons vs FontAwesome 4)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Material Icons → FontAwesome 4 class mapping
|
||||
*/
|
||||
private static $fa4Map = [
|
||||
'account_tree' => 'icon-sitemap',
|
||||
'add' => 'icon-plus',
|
||||
'add_box' => 'icon-plus-square',
|
||||
'arrow_downward' => 'icon-sort-desc',
|
||||
'arrow_drop_down' => 'icon-caret-down',
|
||||
'arrow_right' => 'icon-chevron-right',
|
||||
'arrow_upward' => 'icon-sort-asc',
|
||||
'block' => 'icon-ban',
|
||||
'brush' => 'icon-paint-brush',
|
||||
'business' => 'icon-building',
|
||||
'check' => 'icon-check',
|
||||
'check_box' => 'icon-check-square',
|
||||
'check_box_outline_blank' => 'icon-square-o',
|
||||
'check_circle' => 'icon-check-circle',
|
||||
'close' => 'icon-times',
|
||||
'delete' => 'icon-trash',
|
||||
'description' => 'icon-file-text',
|
||||
'error' => 'icon-exclamation-circle',
|
||||
'event' => 'icon-calendar',
|
||||
'event_busy' => 'icon-calendar-times-o',
|
||||
'expand_less' => 'icon-chevron-up',
|
||||
'expand_more' => 'icon-chevron-down',
|
||||
'filter_list' => 'icon-filter',
|
||||
'flag' => 'icon-flag',
|
||||
'folder' => 'icon-folder',
|
||||
'folder_open' => 'icon-folder-open',
|
||||
'indeterminate_check_box' => 'icon-minus-square',
|
||||
'info' => 'icon-info-circle',
|
||||
'inventory_2' => 'icon-archive',
|
||||
'label' => 'icon-tag',
|
||||
'language' => 'icon-globe',
|
||||
'lightbulb' => 'icon-lightbulb-o',
|
||||
'list' => 'icon-list',
|
||||
'list_alt' => 'icon-list-alt',
|
||||
'local_shipping' => 'icon-truck',
|
||||
'lock' => 'icon-lock',
|
||||
'my_location' => 'icon-crosshairs',
|
||||
'open_in_full' => 'icon-expand',
|
||||
'payments' => 'icon-credit-card',
|
||||
'progress_activity' => 'icon-circle-o-notch',
|
||||
'schedule' => 'icon-clock-o',
|
||||
'search' => 'icon-search',
|
||||
'shopping_cart' => 'icon-shopping-cart',
|
||||
'sort' => 'icon-sort',
|
||||
'sort_by_alpha' => 'icon-sort-alpha-asc',
|
||||
'star' => 'icon-star',
|
||||
'sync' => 'icon-refresh',
|
||||
'tune' => 'icon-sliders',
|
||||
'visibility' => 'icon-eye',
|
||||
'warning' => 'icon-warning',
|
||||
'widgets' => 'icon-th-large',
|
||||
];
|
||||
|
||||
/**
|
||||
* FontAwesome 4 class → Material Icons reverse mapping.
|
||||
* Built once from $fa4Map on first use.
|
||||
* @var array|null
|
||||
*/
|
||||
private static $reverseFa4Map = null;
|
||||
|
||||
/**
|
||||
* Extra FA4→Material mappings for icon names used in block configs
|
||||
* that don't appear in the standard fa4Map (e.g. icon-cube, icon-folder-o).
|
||||
*/
|
||||
private static $extraReverseMappings = [
|
||||
'icon-cube' => 'inventory',
|
||||
'icon-folder-o' => 'folder',
|
||||
'icon-file-text-o' => 'description',
|
||||
'icon-briefcase' => 'work',
|
||||
'icon-user' => 'person',
|
||||
'icon-users' => 'group',
|
||||
'icon-money' => 'payments',
|
||||
'icon-tasks' => 'checklist',
|
||||
'icon-calculator' => 'calculate',
|
||||
'icon-asterisk' => 'star',
|
||||
'icon-bar-chart' => 'bar_chart',
|
||||
'icon-cogs' => 'settings',
|
||||
'icon-cog' => 'settings',
|
||||
'icon-tags' => 'label',
|
||||
'icon-list-ul' => 'list',
|
||||
'icon-th' => 'grid_view',
|
||||
'icon-certificate' => 'verified',
|
||||
'icon-power-off' => 'power_settings_new',
|
||||
'icon-circle-o' => 'radio_button_unchecked',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the reverse FA4→Material mapping (built lazily from $fa4Map + extras).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function getReverseFa4Map()
|
||||
{
|
||||
if (self::$reverseFa4Map === null) {
|
||||
self::$reverseFa4Map = array_flip(self::$fa4Map);
|
||||
// Merge extras (extras take priority for icons not in the flipped map)
|
||||
foreach (self::$extraReverseMappings as $fa4Class => $materialName) {
|
||||
if (!isset(self::$reverseFa4Map[$fa4Class])) {
|
||||
self::$reverseFa4Map[$fa4Class] = $materialName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return self::$reverseFa4Map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an icon name to the canonical format for the current mode.
|
||||
* Handles both Material Icons names and FA4 class names as input.
|
||||
*
|
||||
* @param string $name Icon name (Material or FA4 format)
|
||||
* @return array ['name' => string, 'extra' => string] normalized name + any extra classes
|
||||
*/
|
||||
protected function normalizeIconName($name)
|
||||
{
|
||||
$extra = '';
|
||||
|
||||
// If name starts with 'icon-', it's an FA4 class name
|
||||
if (strpos($name, 'icon-') === 0) {
|
||||
// Extract extra CSS classes (e.g. "icon-power-off text-success" → "icon-power-off" + "text-success")
|
||||
$parts = explode(' ', $name, 2);
|
||||
$fa4Class = $parts[0];
|
||||
if (isset($parts[1])) {
|
||||
$extra = $parts[1];
|
||||
}
|
||||
|
||||
if ($this->getIconMode() === 'material') {
|
||||
// Reverse map FA4→Material
|
||||
$reverseMap = self::getReverseFa4Map();
|
||||
$materialName = $reverseMap[$fa4Class] ?? null;
|
||||
if ($materialName) {
|
||||
return ['name' => $materialName, 'extra' => $extra];
|
||||
}
|
||||
// Last resort: strip 'icon-' prefix and convert hyphens to underscores
|
||||
$fallback = str_replace('-', '_', substr($fa4Class, 5));
|
||||
return ['name' => $fallback, 'extra' => $extra];
|
||||
}
|
||||
|
||||
// Already FA4 and mode is FA4 — use as-is
|
||||
return ['name' => $fa4Class, 'extra' => $extra, 'raw_fa4' => true];
|
||||
}
|
||||
|
||||
// Material Icons name — use as-is for material mode, map for FA4 mode
|
||||
return ['name' => $name, 'extra' => $extra];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect icon mode based on PrestaShop version.
|
||||
* PS 8+ / 9+ → material, PS 1.6 / 1.7 → fa4.
|
||||
*
|
||||
* @return string 'material' or 'fa4'
|
||||
*/
|
||||
protected function getIconMode()
|
||||
{
|
||||
return version_compare(_PS_VERSION_, '8.0.0', '>=') ? 'material' : 'fa4';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an icon element that works on both legacy and modern PS.
|
||||
* Accepts both Material Icons names and FA4 class names as input.
|
||||
*
|
||||
* @param string $name Icon name (Material or FA4 format, e.g. 'shopping_cart' or 'icon-cube')
|
||||
* @param string $extraClass Additional CSS class(es)
|
||||
* @return string HTML
|
||||
*/
|
||||
protected function renderIcon($name, $extraClass = '')
|
||||
{
|
||||
$normalized = $this->normalizeIconName($name);
|
||||
$iconName = $normalized['name'];
|
||||
// Merge extra classes from normalization (e.g. "text-success" from "icon-power-off text-success")
|
||||
if (!empty($normalized['extra'])) {
|
||||
$extraClass = $extraClass ? $extraClass . ' ' . $normalized['extra'] : $normalized['extra'];
|
||||
}
|
||||
|
||||
if ($this->getIconMode() === 'material') {
|
||||
$cls = 'material-icons es-icon';
|
||||
if ($extraClass) {
|
||||
$cls .= ' ' . $extraClass;
|
||||
}
|
||||
return '<i class="' . $cls . '">' . htmlspecialchars($iconName, ENT_QUOTES, 'UTF-8') . '</i>';
|
||||
}
|
||||
|
||||
// FA4 mode
|
||||
if (!empty($normalized['raw_fa4'])) {
|
||||
// Input was already an FA4 class name — use directly
|
||||
$cls = $iconName . ' es-icon';
|
||||
} else {
|
||||
// Input was a Material name — map to FA4
|
||||
$mapped = self::$fa4Map[$iconName] ?? 'icon-circle';
|
||||
$cls = $mapped . ' es-icon';
|
||||
}
|
||||
if ($extraClass) {
|
||||
$cls .= ' ' . $extraClass;
|
||||
}
|
||||
return '<i class="' . $cls . '"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set block definitions
|
||||
*
|
||||
@@ -303,6 +99,21 @@ class EntitySelectorRenderer
|
||||
return htmlspecialchars((string)$string, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a help icon with tooltip or details popover.
|
||||
* Single source of truth for info icons across the entity selector.
|
||||
*
|
||||
* @param string $content HTML content for the tooltip/popover
|
||||
* @param string $type 'tooltip' for hover tooltip, 'details' for click popover
|
||||
* @return string HTML string
|
||||
*/
|
||||
protected function buildHelpIcon($content, $type = 'tooltip')
|
||||
{
|
||||
$attr = ($type === 'details') ? 'data-details' : 'data-tooltip';
|
||||
return '<span class="mpr-info-wrapper" ' . $attr . '="' . $this->escapeAttr($content) . '">'
|
||||
. '<i class="material-icons">info_outline</i></span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render target conditions HTML
|
||||
*
|
||||
@@ -325,6 +136,7 @@ class EntitySelectorRenderer
|
||||
'show_cms' => true,
|
||||
'show_cms_categories' => true,
|
||||
'combination_mode' => 'products',
|
||||
'product_selection_level' => 'product',
|
||||
'mode' => 'multi',
|
||||
'blocks' => [],
|
||||
'customBlocks' => [],
|
||||
@@ -343,6 +155,9 @@ class EntitySelectorRenderer
|
||||
if (is_string($savedData)) {
|
||||
$savedData = json_decode($savedData, true) ?: [];
|
||||
}
|
||||
if (!is_array($savedData)) {
|
||||
$savedData = [];
|
||||
}
|
||||
|
||||
// Determine which block is active
|
||||
$enabledBlocks = [];
|
||||
@@ -366,7 +181,7 @@ class EntitySelectorRenderer
|
||||
'label' => $blockDef['label'] ?? $blockType,
|
||||
'entity_label' => $blockDef['label'] ?? $blockType,
|
||||
'entity_label_plural' => $blockDef['label'] ?? $blockType,
|
||||
'icon' => $blockDef['icon'] ?? 'settings',
|
||||
'icon' => $blockDef['icon'] ?? 'icon-cog',
|
||||
'search_entity' => $blockType,
|
||||
'selection_methods' => [],
|
||||
], $blockDef);
|
||||
@@ -414,10 +229,9 @@ class EntitySelectorRenderer
|
||||
}
|
||||
|
||||
// Standalone layout (default)
|
||||
$html = '<div class="condition-trait target-conditions-trait' . $collapsedClass . $singleModeClass . $requiredClass . $layoutClass . '"';
|
||||
$html = '<div class="condition-trait entity-selector-trait' . $collapsedClass . $singleModeClass . $requiredClass . $layoutClass . '"';
|
||||
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
|
||||
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
|
||||
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
|
||||
if (!empty($config['required'])) {
|
||||
$html .= ' data-required="1"';
|
||||
$requiredMsg = !empty($config['required_message'])
|
||||
@@ -431,8 +245,8 @@ class EntitySelectorRenderer
|
||||
$html .= $this->renderHeader($config, $hasAnyData, $globalMode);
|
||||
|
||||
// Body
|
||||
$bodyStyle = !empty($collapsedClass) ? ' style="display:none;"' : '';
|
||||
$html .= '<div class="condition-trait-body"' . $bodyStyle . '>';
|
||||
$bodyExpandedClass = empty($collapsedClass) ? ' es-expanded' : '';
|
||||
$html .= '<div class="condition-trait-body' . $bodyExpandedClass . '">';
|
||||
|
||||
// Tabs
|
||||
$html .= $this->renderTabs($enabledBlocks, $activeBlock, $savedData, $config);
|
||||
@@ -447,8 +261,7 @@ class EntitySelectorRenderer
|
||||
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
|
||||
|
||||
$html .= '</div>'; // End condition-trait-body
|
||||
|
||||
$html .= '</div>'; // End target-conditions-trait
|
||||
$html .= '</div>'; // End entity-selector-trait
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -483,10 +296,9 @@ class EntitySelectorRenderer
|
||||
$html .= '<div class="col-lg-9">';
|
||||
|
||||
// Entity selector container (without traditional header)
|
||||
$html .= '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . '"';
|
||||
$html .= '<div class="condition-trait entity-selector-trait layout-form-group' . $singleModeClass . $requiredClass . '"';
|
||||
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
|
||||
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
|
||||
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
|
||||
if (!empty($config['required'])) {
|
||||
$html .= ' data-required="1"';
|
||||
$requiredMsg = !empty($config['required_message'])
|
||||
@@ -497,7 +309,7 @@ class EntitySelectorRenderer
|
||||
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
|
||||
|
||||
// Body (always visible in form-group layout)
|
||||
$html .= '<div class="condition-trait-body">';
|
||||
$html .= '<div class="condition-trait-body es-expanded">';
|
||||
|
||||
// Tabs row with expand button
|
||||
$html .= '<div class="entity-selector-tabs-row">';
|
||||
@@ -507,7 +319,7 @@ class EntitySelectorRenderer
|
||||
if ($globalMode !== 'single') {
|
||||
$html .= '<div class="entity-selector-actions">';
|
||||
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
|
||||
$html .= $this->renderIcon('open_in_full');
|
||||
$html .= '<i class="icon-resize-vertical"></i>';
|
||||
$html .= '</button>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
@@ -523,8 +335,7 @@ class EntitySelectorRenderer
|
||||
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
|
||||
|
||||
$html .= '</div>'; // End condition-trait-body
|
||||
|
||||
$html .= '</div>'; // End target-conditions-trait
|
||||
$html .= '</div>'; // End entity-selector-trait
|
||||
|
||||
// Subtitle as help text
|
||||
if (!empty($config['subtitle'])) {
|
||||
@@ -559,10 +370,9 @@ class EntitySelectorRenderer
|
||||
$collapsedClass = $collapsed ? ' blocks-collapsed' : '';
|
||||
|
||||
// Entity selector container (without form-group wrapper, without header)
|
||||
$html = '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . $collapsedClass . '"';
|
||||
$html = '<div class="condition-trait entity-selector-trait layout-form-group' . $singleModeClass . $requiredClass . $collapsedClass . '"';
|
||||
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
|
||||
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
|
||||
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
|
||||
if (!empty($config['required'])) {
|
||||
$html .= ' data-required="1"';
|
||||
$requiredMsg = !empty($config['required_message'])
|
||||
@@ -578,13 +388,13 @@ class EntitySelectorRenderer
|
||||
|
||||
// Actions: expand/collapse toggle (entire area is clickable)
|
||||
$html .= '<div class="entity-selector-actions btn-toggle-blocks" title="' . $this->trans('Show/hide details') . '">';
|
||||
$html .= $this->renderIcon($collapsed ? 'expand_more' : 'expand_less');
|
||||
$html .= '<i class="' . ($collapsed ? 'icon-chevron-down' : 'icon-chevron-up') . '"></i>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>'; // End tabs-row
|
||||
|
||||
// Blocks content (visible by default when not collapsed)
|
||||
$blocksStyle = $collapsed ? ' style="display:none;"' : '';
|
||||
$html .= '<div class="entity-selector-blocks-content"' . $blocksStyle . '>';
|
||||
$blocksExpandedClass = $collapsed ? '' : ' es-expanded';
|
||||
$html .= '<div class="entity-selector-blocks-content' . $blocksExpandedClass . '">';
|
||||
|
||||
// Blocks
|
||||
$html .= $this->renderBlocks($enabledBlocks, $activeBlock, $savedData, $config, $globalMode);
|
||||
@@ -597,8 +407,7 @@ class EntitySelectorRenderer
|
||||
// Hidden input (outside collapsed area)
|
||||
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
|
||||
|
||||
|
||||
$html .= '</div>'; // End target-conditions-trait
|
||||
$html .= '</div>'; // End entity-selector-trait
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -615,12 +424,12 @@ class EntitySelectorRenderer
|
||||
{
|
||||
$html = '<div class="condition-trait-header">';
|
||||
$html .= '<div class="trait-header-left">';
|
||||
$html .= $this->renderIcon('my_location', 'trait-icon');
|
||||
$html .= '<i class="icon-crosshairs trait-icon"></i>';
|
||||
$html .= '<div class="trait-title-group">';
|
||||
$html .= '<span class="trait-title">' . $this->escapeAttr($config['title']) . '</span>';
|
||||
$html .= '<span class="trait-subtitle">' . $this->escapeAttr($config['subtitle']) . '</span>';
|
||||
$html .= '</div>';
|
||||
$html .= '<span class="trait-total-count" style="display: none;" title="' . $this->trans('Total items targeted') . '">' . $this->renderIcon('visibility') . ' <span class="count-value"></span></span>';
|
||||
$html .= '<span class="trait-total-count" style="display: none;" title="' . $this->trans('Total items targeted') . '"><i class="icon-eye"></i> <span class="count-value"></span></span>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="trait-header-right">';
|
||||
|
||||
@@ -635,7 +444,7 @@ class EntitySelectorRenderer
|
||||
if ($globalMode !== 'single') {
|
||||
$html .= '<div class="trait-header-actions">';
|
||||
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
|
||||
$html .= $this->renderIcon('open_in_full');
|
||||
$html .= '<i class="icon-expand"></i>';
|
||||
$html .= '</button>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
@@ -677,17 +486,17 @@ class EntitySelectorRenderer
|
||||
|
||||
if ($emptyMeansAllToggle) {
|
||||
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_on" value="1"';
|
||||
$html .= ' class="target-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= ' class="es-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= '<label for="' . $switchName . '_on">' . $this->escapeAttr($emptyLabel) . '</label>';
|
||||
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_off" value="0"';
|
||||
$html .= ' class="target-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= ' class="es-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= '<label for="' . $switchName . '_off">' . $this->escapeAttr($selectedLabel) . '</label>';
|
||||
} else {
|
||||
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_on" value="0"';
|
||||
$html .= ' class="target-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= ' class="es-switch-toggle"' . ($hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= '<label for="' . $switchName . '_on">' . $this->escapeAttr($selectedLabel) . '</label>';
|
||||
$html .= '<input type="radio" name="' . $switchName . '" id="' . $switchName . '_off" value="1"';
|
||||
$html .= ' class="target-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= ' class="es-switch-toggle"' . (!$hasAnyData ? ' checked' : '') . '>';
|
||||
$html .= '<label for="' . $switchName . '_off">' . $this->escapeAttr($emptyLabel) . '</label>';
|
||||
}
|
||||
|
||||
@@ -709,7 +518,7 @@ class EntitySelectorRenderer
|
||||
protected function renderTabs($enabledBlocks, $activeBlock, $savedData, $config)
|
||||
{
|
||||
$blockSettings = $config['blocks'] ?? [];
|
||||
$html = '<div class="target-block-tabs">';
|
||||
$html = '<div class="es-block-tabs">';
|
||||
|
||||
foreach ($enabledBlocks as $blockType => $blockDef) {
|
||||
$isActive = ($blockType === $activeBlock);
|
||||
@@ -718,12 +527,12 @@ class EntitySelectorRenderer
|
||||
$hasDataClass = $hasData ? ' has-data' : '';
|
||||
$blockMode = $blockSettings[$blockType]['mode'] ?? 'multi';
|
||||
|
||||
$html .= '<button type="button" class="target-block-tab' . $activeClass . $hasDataClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-block-mode="' . $this->escapeAttr($blockMode) . '">';
|
||||
$html .= $this->renderIcon($blockDef['icon']);
|
||||
$html .= '<button type="button" class="es-block-tab' . $activeClass . $hasDataClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-block-mode="' . $this->escapeAttr($blockMode) . '">';
|
||||
$html .= '<i class="' . $this->escapeAttr($blockDef['icon']) . '"></i>';
|
||||
$html .= '<span class="tab-label">' . $this->escapeAttr($blockDef['label']) . '</span>';
|
||||
if ($hasData) {
|
||||
// Show loading spinner that will be replaced with actual count
|
||||
$html .= '<span class="tab-badge loading">' . $this->renderIcon('sync', 'es-spin') . '</span>';
|
||||
$html .= '<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>';
|
||||
}
|
||||
$html .= '</button>';
|
||||
}
|
||||
@@ -746,7 +555,7 @@ class EntitySelectorRenderer
|
||||
{
|
||||
$blockSettings = $config['blocks'] ?? [];
|
||||
$emptyMeansAll = $config['empty_means_all'] ?? true;
|
||||
$html = '<div class="target-blocks-wrapper">';
|
||||
$html = '<div class="es-blocks-wrapper">';
|
||||
|
||||
foreach ($enabledBlocks as $blockType => $blockDef) {
|
||||
$isActive = ($blockType === $activeBlock);
|
||||
@@ -766,18 +575,18 @@ class EntitySelectorRenderer
|
||||
*/
|
||||
protected function renderTipsBox()
|
||||
{
|
||||
$html = '<div class="target-tips-box">';
|
||||
$html = '<div class="es-tips-box">';
|
||||
$html .= '<div class="tips-header">';
|
||||
$html .= $this->renderIcon('lightbulb');
|
||||
$html .= '<i class="icon-lightbulb-o"></i>';
|
||||
$html .= '<span>' . $this->trans('Pro Tips: Combine include & exclude for powerful targeting') . '</span>';
|
||||
$html .= $this->renderIcon('expand_more', 'tips-toggle');
|
||||
$html .= '<i class="icon-chevron-down tips-toggle"></i>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="tips-content">';
|
||||
$html .= '<div class="tips-grid">';
|
||||
|
||||
// Example 1
|
||||
$html .= '<div class="tip-item">';
|
||||
$html .= '<div class="tip-icon">' . $this->renderIcon('check_circle') . '</div>';
|
||||
$html .= '<div class="tip-icon"><i class="icon-check-circle"></i></div>';
|
||||
$html .= '<div class="tip-text">';
|
||||
$html .= '<strong>' . $this->trans('Target entire catalog with exceptions') . '</strong>';
|
||||
$html .= '<p>' . $this->trans('Select "All products", then exclude specific categories like "Sale" or "Clearance" where you don\'t want the rule to apply.') . '</p>';
|
||||
@@ -786,7 +595,7 @@ class EntitySelectorRenderer
|
||||
|
||||
// Example 2
|
||||
$html .= '<div class="tip-item">';
|
||||
$html .= '<div class="tip-icon">' . $this->renderIcon('filter_list') . '</div>';
|
||||
$html .= '<div class="tip-icon"><i class="icon-filter"></i></div>';
|
||||
$html .= '<div class="tip-text">';
|
||||
$html .= '<strong>' . $this->trans('Combine features for precise filtering') . '</strong>';
|
||||
$html .= '<p>' . $this->trans('Target all "Cotton" products, then exclude those with "Black" color feature. Perfect for material-specific promotions.') . '</p>';
|
||||
@@ -795,7 +604,7 @@ class EntitySelectorRenderer
|
||||
|
||||
// Example 3
|
||||
$html .= '<div class="tip-item">';
|
||||
$html .= '<div class="tip-icon">' . $this->renderIcon('account_tree') . '</div>';
|
||||
$html .= '<div class="tip-icon"><i class="icon-sitemap"></i></div>';
|
||||
$html .= '<div class="tip-text">';
|
||||
$html .= '<strong>' . $this->trans('Category-based targeting') . '</strong>';
|
||||
$html .= '<p>' . $this->trans('Include entire "Men\'s Clothing" category, exclude "Accessories" subcategory. Hierarchy is respected automatically.') . '</p>';
|
||||
@@ -804,7 +613,7 @@ class EntitySelectorRenderer
|
||||
|
||||
// Example 4
|
||||
$html .= '<div class="tip-item">';
|
||||
$html .= '<div class="tip-icon">' . $this->renderIcon('business') . '</div>';
|
||||
$html .= '<div class="tip-icon"><i class="icon-building"></i></div>';
|
||||
$html .= '<div class="tip-text">';
|
||||
$html .= '<strong>' . $this->trans('Brand exclusions') . '</strong>';
|
||||
$html .= '<p>' . $this->trans('Target all products from "Nike" manufacturer, but exclude items already on sale (by price range or specific products).') . '</p>';
|
||||
@@ -813,11 +622,11 @@ class EntitySelectorRenderer
|
||||
|
||||
$html .= '</div>'; // End tips-grid
|
||||
$html .= '<div class="tips-footer">';
|
||||
$html .= $this->renderIcon('info') . ' ';
|
||||
$html .= '<i class="material-icons">info_outline</i> ';
|
||||
$html .= $this->trans('Multiple groups work as OR logic. Items matching ANY group are included (unless explicitly excluded in that group).');
|
||||
$html .= '</div>';
|
||||
$html .= '</div>'; // End tips-content
|
||||
$html .= '</div>'; // End target-tips-box
|
||||
$html .= '</div>'; // End es-tips-box
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -843,7 +652,7 @@ class EntitySelectorRenderer
|
||||
$isCustomBlock = !empty($blockDef['custom']);
|
||||
$customClass = $isCustomBlock ? ' custom-block' : '';
|
||||
|
||||
$html = '<div class="target-block' . $activeClass . $modeClass . $customClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-mode="' . $this->escapeAttr($mode) . '"' . $displayStyle . '>';
|
||||
$html = '<div class="es-block' . $activeClass . $modeClass . $customClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-mode="' . $this->escapeAttr($mode) . '"' . $displayStyle . '>';
|
||||
$html .= '<div class="block-body">';
|
||||
|
||||
// Custom blocks render their own HTML
|
||||
@@ -913,16 +722,14 @@ class EntitySelectorRenderer
|
||||
|
||||
$html .= '<div class="block-footer">';
|
||||
$html .= '<button type="button" class="btn-add-group">';
|
||||
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add selection group');
|
||||
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add selection group');
|
||||
$html .= '</button>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($groupsTooltip) . '">';
|
||||
$html .= $this->renderIcon('info');
|
||||
$html .= '</span>';
|
||||
$html .= $this->buildHelpIcon($groupsTooltip, 'details');
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>'; // End block-body
|
||||
$html .= '</div>'; // End target-block
|
||||
$html .= '</div>'; // End es-block
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -974,12 +781,10 @@ class EntitySelectorRenderer
|
||||
|
||||
$html = '<div class="result-modifiers-section">';
|
||||
$html .= '<div class="result-modifiers-header">';
|
||||
$html .= $this->renderIcon('tune') . ' ';
|
||||
$html .= '<i class="icon-sliders"></i> ';
|
||||
$html .= '<span class="result-modifiers-title">' . $this->trans('Result modifiers') . '</span>';
|
||||
$html .= '<span class="result-modifiers-hint">' . $this->trans('(optional)') . '</span>';
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($modifiersTooltip) . '">';
|
||||
$html .= $this->renderIcon('info');
|
||||
$html .= '</span>';
|
||||
$html .= $this->buildHelpIcon($modifiersTooltip, 'details');
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="result-modifiers-content">';
|
||||
@@ -1117,17 +922,17 @@ class EntitySelectorRenderer
|
||||
// Group header
|
||||
if ($mode === 'single') {
|
||||
$html .= '<div class="group-header group-header-single">';
|
||||
$html .= '<span class="group-count-badge" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
|
||||
$html .= '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
|
||||
$html .= '</div>';
|
||||
} else {
|
||||
$html .= '<div class="group-header">';
|
||||
$html .= '<span class="group-collapse-toggle">' . $this->renderIcon('expand_less') . '</span>';
|
||||
$html .= '<span class="group-collapse-toggle"><i class="icon-chevron-up"></i></span>';
|
||||
$html .= '<span class="group-name-wrapper">';
|
||||
$html .= '<input type="text" class="group-name-input" value="' . $groupName . '" placeholder="' . $defaultGroupName . '" title="' . $this->trans('Click to name this group') . '">';
|
||||
$html .= '<span class="group-count-badge" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
|
||||
$html .= '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
|
||||
$html .= '</span>';
|
||||
$html .= '<button type="button" class="btn-remove-group" title="' . $this->trans('Remove group') . '">';
|
||||
$html .= $this->renderIcon('delete');
|
||||
$html .= '<i class="icon-trash"></i>';
|
||||
$html .= '</button>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
@@ -1144,15 +949,13 @@ class EntitySelectorRenderer
|
||||
$methodHelp = $this->getMethodHelpTooltip($includeMethod, $blockType);
|
||||
$html .= '<span class="method-info-placeholder">';
|
||||
if (!empty($methodHelp)) {
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
|
||||
$html .= $this->renderIcon('info');
|
||||
$html .= '</span>';
|
||||
$html .= $this->buildHelpIcon($methodHelp, 'details');
|
||||
}
|
||||
$html .= '</span>';
|
||||
$html .= '<select class="include-method-select">';
|
||||
$html .= $this->renderMethodOptions($methods, $includeMethod, false);
|
||||
$html .= '</select>';
|
||||
$html .= '<span class="condition-match-count no-matches">' . $this->renderIcon('visibility') . ' <span class="preview-count">0</span></span>';
|
||||
$html .= '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Value picker
|
||||
@@ -1167,7 +970,7 @@ class EntitySelectorRenderer
|
||||
|
||||
if ($hasExcludes) {
|
||||
$html .= '<div class="except-separator">';
|
||||
$html .= '<span class="except-label">' . $this->renderIcon('block') . ' ' . $this->trans('EXCEPT') . '</span>';
|
||||
$html .= '<span class="except-label"><i class="icon-ban"></i> ' . $this->trans('EXCEPT') . '</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="exclude-rows-container">';
|
||||
@@ -1177,11 +980,11 @@ class EntitySelectorRenderer
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<button type="button" class="btn-add-another-exclude">';
|
||||
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add another exception');
|
||||
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add another exception');
|
||||
$html .= '</button>';
|
||||
} else {
|
||||
$html .= '<button type="button" class="btn-add-exclude">';
|
||||
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add exceptions');
|
||||
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add exceptions');
|
||||
$html .= '</button>';
|
||||
}
|
||||
|
||||
@@ -1239,15 +1042,15 @@ class EntitySelectorRenderer
|
||||
$html .= '<option value="' . $this->escapeAttr($value) . '"' . $selected . '>' . $this->escapeAttr($label) . '</option>';
|
||||
}
|
||||
$html .= '</select>';
|
||||
$sortDirIcon = ($sortDir === 'ASC') ? 'arrow_upward' : 'arrow_downward';
|
||||
$sortDirIcon = ($sortDir === 'ASC') ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
|
||||
$html .= '<button type="button" class="btn-sort-dir" data-dir="' . $this->escapeAttr($sortDir) . '" title="' . $this->trans('Sort direction') . '">';
|
||||
$html .= $this->renderIcon($sortDirIcon);
|
||||
$html .= '<i class="' . $sortDirIcon . '"></i>';
|
||||
$html .= '</button>';
|
||||
$html .= '</span>';
|
||||
|
||||
// Preview badge
|
||||
$html .= '<span class="group-preview-badge clickable" title="' . $this->trans('Preview results') . '">';
|
||||
$html .= $this->renderIcon('visibility') . ' <span class="preview-count"></span>';
|
||||
$html .= '<i class="icon-eye"></i> <span class="preview-count"></span>';
|
||||
$html .= '</span>';
|
||||
|
||||
$html .= '</div>';
|
||||
@@ -1361,9 +1164,7 @@ class EntitySelectorRenderer
|
||||
$methodHelp = $this->getMethodHelpTooltip($excludeMethod, $blockType);
|
||||
$html .= '<span class="method-info-placeholder">';
|
||||
if (!empty($methodHelp)) {
|
||||
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
|
||||
$html .= $this->renderIcon('info');
|
||||
$html .= '</span>';
|
||||
$html .= $this->buildHelpIcon($methodHelp, 'details');
|
||||
}
|
||||
$html .= '</span>';
|
||||
|
||||
@@ -1371,11 +1172,11 @@ class EntitySelectorRenderer
|
||||
$html .= $this->renderMethodOptions($methods, $excludeMethod, true);
|
||||
$html .= '</select>';
|
||||
|
||||
$html .= '<span class="condition-match-count no-matches">' . $this->renderIcon('visibility') . ' <span class="preview-count">0</span></span>';
|
||||
$html .= '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
|
||||
$html .= '</div>'; // End method-selector-wrapper
|
||||
|
||||
$html .= '<button type="button" class="btn-remove-exclude-row" title="' . $this->trans('Remove this exception') . '">';
|
||||
$html .= $this->renderIcon('delete');
|
||||
$html .= '<i class="icon-trash"></i>';
|
||||
$html .= '</button>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -1418,12 +1219,15 @@ class EntitySelectorRenderer
|
||||
switch ($valueType) {
|
||||
case 'entity_search':
|
||||
$noItemsPlaceholder = $this->trans('No items selected - use search below');
|
||||
// Don't pre-wrap chips - JS will create the wrapper with toolbar when chips are added
|
||||
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . $this->escapeAttr($noItemsPlaceholder) . '"></div>';
|
||||
$html .= '<div class="chips-wrapper">';
|
||||
$html .= '<div class="chips-toolbar" style="display:none;"></div>';
|
||||
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . $this->escapeAttr($noItemsPlaceholder) . '"><span class="chips-empty-state">' . htmlspecialchars($noItemsPlaceholder) . '</span></div>';
|
||||
$html .= '<div class="chips-load-more" style="display:none;"></div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="entity-search-box">';
|
||||
$html .= $this->renderIcon('search', 'entity-search-icon');
|
||||
$html .= '<i class="icon-search entity-search-icon"></i>';
|
||||
$html .= '<input type="text" class="entity-search-input" placeholder="' . $this->trans('Search by name, reference, ID...') . '" autocomplete="off">';
|
||||
$html .= '<span class="search-loading" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
|
||||
$html .= '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
|
||||
$html .= '</div>';
|
||||
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($values)) . '">';
|
||||
break;
|
||||
@@ -1435,7 +1239,7 @@ class EntitySelectorRenderer
|
||||
$html .= '<button type="button" class="btn-toggle-case" title="' . $this->escapeAttr($this->trans('Case insensitive - click to toggle')) . '"><span class="case-icon">aa</span></button>';
|
||||
$html .= '<input type="text" class="pattern-input" value="" placeholder="' . $this->escapeAttr($this->trans('e.g. *cotton*')) . '">';
|
||||
$html .= '<span class="pattern-match-count"></span>';
|
||||
$html .= '<button type="button" class="btn-add-pattern" title="' . $this->escapeAttr($this->trans('Add pattern (Enter)')) . '">' . $this->renderIcon('add') . '</button>';
|
||||
$html .= '<button type="button" class="btn-add-pattern" title="' . $this->escapeAttr($this->trans('Add pattern (Enter)')) . '"><i class="icon-plus"></i></button>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($values)) . '">';
|
||||
@@ -1464,7 +1268,7 @@ class EntitySelectorRenderer
|
||||
$html .= '<input type="number" class="range-min-input" value="" placeholder="' . $this->trans('Min') . '" step="0.01">';
|
||||
$html .= '<span class="range-separator">-</span>';
|
||||
$html .= '<input type="number" class="range-max-input" value="" placeholder="' . $this->trans('Max') . '" step="0.01">';
|
||||
$html .= '<button type="button" class="btn-add-range" title="' . $this->trans('Add range') . '">' . $this->renderIcon('add') . '</button>';
|
||||
$html .= '<button type="button" class="btn-add-range" title="' . $this->trans('Add range') . '"><i class="icon-plus"></i></button>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($ranges)) . '">';
|
||||
@@ -1481,7 +1285,7 @@ class EntitySelectorRenderer
|
||||
$optIcon = is_array($optData) ? ($optData['icon'] ?? '') : '';
|
||||
$html .= '<button type="button" class="tile-option' . $selectedClass . '" data-value="' . $this->escapeAttr($optKey) . '">';
|
||||
if ($optIcon) {
|
||||
$html .= $this->renderIcon($optIcon) . ' ';
|
||||
$html .= '<i class="' . $this->escapeAttr($optIcon) . '"></i> ';
|
||||
}
|
||||
$html .= '<span class="tile-label">' . $this->escapeAttr($optLabel) . '</span>';
|
||||
$html .= '</button>';
|
||||
@@ -1752,6 +1556,7 @@ class EntitySelectorRenderer
|
||||
],
|
||||
'methodHelp' => $this->getAllMethodHelpContent(),
|
||||
'combinationMode' => $config['combination_mode'] ?? 'products',
|
||||
'productSelectionLevel' => $config['product_selection_level'] ?? 'product',
|
||||
'emptyMeansAll' => $config['empty_means_all'] ?? true,
|
||||
];
|
||||
}
|
||||
@@ -1787,23 +1592,23 @@ class EntitySelectorRenderer
|
||||
|
||||
switch ($sortBy) {
|
||||
case 'name':
|
||||
return $isAsc ? 'sort_by_alpha' : 'sort_by_alpha';
|
||||
return $isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
|
||||
case 'price':
|
||||
case 'quantity':
|
||||
case 'product_count':
|
||||
return $isAsc ? 'sort' : 'sort';
|
||||
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
|
||||
case 'date_add':
|
||||
case 'newest_products':
|
||||
return $isAsc ? 'sort' : 'sort';
|
||||
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
|
||||
case 'sales':
|
||||
case 'total_sales':
|
||||
return $isAsc ? 'arrow_upward' : 'arrow_downward';
|
||||
return $isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
|
||||
case 'position':
|
||||
return $isAsc ? 'sort' : 'sort';
|
||||
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
|
||||
case 'random':
|
||||
return 'shuffle';
|
||||
return 'icon-random';
|
||||
default:
|
||||
return $isAsc ? 'arrow_upward' : 'arrow_downward';
|
||||
return $isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ trait ScheduleConditions
|
||||
|
||||
$assetPath = $this->getScheduleConditionsAssetPath();
|
||||
|
||||
// Load combined Tailwind CSS (includes target-conditions, modal, condition-traits, list-preview)
|
||||
// Load combined Tailwind CSS (includes entity-selector, modal, condition-traits, list-preview)
|
||||
$this->addCSS($assetPath . 'css/admin/tailwind-output.css');
|
||||
$this->addJS($assetPath . 'js/admin/schedule-conditions.js');
|
||||
|
||||
@@ -254,7 +254,7 @@ trait ScheduleConditions
|
||||
$html .= '</div>';
|
||||
|
||||
// Collapsible body
|
||||
$html .= '<div class="condition-trait-body"' . ($scheduleEnabled ? '' : ' style="display:none;"') . '>';
|
||||
$html .= '<div class="condition-trait-body' . ($scheduleEnabled ? ' es-expanded' : '') . '">';
|
||||
|
||||
// Datetime Range Section
|
||||
if ($config['show_datetime_range']) {
|
||||
@@ -761,7 +761,7 @@ trait ScheduleConditions
|
||||
|
||||
/**
|
||||
* Render TargetConditions block for holiday countries
|
||||
* Creates a standalone target-conditions widget for country selection
|
||||
* Creates a standalone entity-selector widget for country selection
|
||||
*
|
||||
* @param string $prefix Form field prefix
|
||||
* @param array $savedData Saved target conditions data
|
||||
@@ -800,16 +800,16 @@ trait ScheduleConditions
|
||||
|
||||
$jsConfigJson = htmlspecialchars(json_encode($jsConfig), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$html = '<div class="target-conditions-trait holiday-countries-target layout-form-group"';
|
||||
$html = '<div class="entity-selector-trait holiday-countries-target layout-form-group"';
|
||||
$html .= ' data-entity-selector-id="holiday-countries-target"';
|
||||
$html .= ' data-mode="multi"';
|
||||
$html .= ' data-config=\'' . $jsConfigJson . '\'>';
|
||||
|
||||
// Hidden input for serialized data
|
||||
$html .= '<input type="hidden" name="' . $prefix . 'holiday_countries_data" class="target-conditions-data" value="' . htmlspecialchars(json_encode($savedData)) . '">';
|
||||
$html .= '<input type="hidden" name="' . $prefix . 'holiday_countries_data" class="es-conditions-data" value="' . htmlspecialchars(json_encode($savedData)) . '">';
|
||||
|
||||
// Target blocks wrapper (single block for countries)
|
||||
$html .= '<div class="target-blocks-wrapper">';
|
||||
$html .= '<div class="es-blocks-wrapper">';
|
||||
$html .= $this->renderHolidayCountriesBlock($blockType, $methods, $groups);
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -826,7 +826,7 @@ trait ScheduleConditions
|
||||
$hasGroups = !empty($groups);
|
||||
$emptyStateText = $this->transScheduleConditions('All countries included');
|
||||
|
||||
$html = '<div class="target-block active" data-block-type="' . htmlspecialchars($blockType) . '" data-mode="multi">';
|
||||
$html = '<div class="es-block active" data-block-type="' . htmlspecialchars($blockType) . '" data-mode="multi">';
|
||||
$html .= '<div class="block-body">';
|
||||
$html .= '<div class="groups-container">';
|
||||
|
||||
@@ -861,7 +861,7 @@ trait ScheduleConditions
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>'; // End block-body
|
||||
$html .= '</div>'; // End target-block
|
||||
$html .= '</div>'; // End es-block
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -1033,7 +1033,11 @@ trait ScheduleConditions
|
||||
switch ($valueType) {
|
||||
case 'entity_search':
|
||||
$noItemsPlaceholder = $this->transScheduleConditions('No items selected - use search below');
|
||||
$html .= '<div class="chips-wrapper">';
|
||||
$html .= '<div class="chips-toolbar"></div>';
|
||||
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . htmlspecialchars($noItemsPlaceholder) . '"></div>';
|
||||
$html .= '<div class="chips-load-more"></div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="entity-search-box">';
|
||||
$html .= '<i class="icon-search entity-search-icon"></i>';
|
||||
$html .= '<input type="text" class="entity-search-input" placeholder="' . $this->transScheduleConditions('Search by name, reference, ID...') . '" autocomplete="off">';
|
||||
|
||||
Reference in New Issue
Block a user