';
+ // Remove empty state
+ $container.find('.groups-empty-state').remove();
- // Header with results count, actions, sort controls, view mode
- html += '';
-
- this.$dropdown = $(html);
- $('body').append(this.$dropdown);
+ this.updateBlockStatus($block);
+ this.serializeAllBlocks();
},
- hideDropdown: function() {
- if (this.$dropdown) {
- this.$dropdown.removeClass('show');
+ removeGroup: function($group, $block) {
+ $group.remove();
+
+ var $container = $block.find('.groups-container');
+ var remainingGroups = $container.find('.selection-group').length;
+
+ if (remainingGroups === 0) {
+ var emptyText = this.getEmptyStateText($block);
+ var emptyHtml = '
';
+ emptyHtml += '' + emptyText + '';
+ emptyHtml += '
';
+ $container.html(emptyHtml);
}
- this.activeGroup = null;
+
+ this.updateBlockStatus($block);
+ this.serializeAllBlocks();
+
+ // Update tab badges and header total count
+ this.updateTabBadges();
},
- positionDropdown: function($input) {
- if (!this.$dropdown) return;
+ clearAllConditions: function() {
+ var self = this;
- var $picker = $input.closest('.value-picker');
- var $searchBox = $input.closest('.entity-search-box');
+ // Remove all groups from all blocks
+ this.$wrapper.find('.es-block').each(function() {
+ var $block = $(this);
+ var $container = $block.find('.groups-container');
- // Get absolute positions (dropdown is appended to body)
- var searchBoxOffset = $searchBox.offset();
- var searchBoxHeight = $searchBox.outerHeight();
- var pickerOffset = $picker.offset();
- var pickerWidth = $picker.outerWidth();
+ // Remove all groups
+ $container.find('.selection-group').remove();
- // Calculate position relative to document
- var dropdownTop = searchBoxOffset.top + searchBoxHeight + 4;
- var dropdownLeft = pickerOffset.left;
- var dropdownWidth = Math.max(pickerWidth, 400);
+ // Show empty state
+ var emptyText = self.getEmptyStateText($block);
+ var emptyHtml = '
';
+ emptyHtml += '' + emptyText + '';
+ emptyHtml += '
';
+ $container.html(emptyHtml);
- // Ensure dropdown doesn't overflow the viewport horizontally
- var viewportWidth = $(window).width();
- if (dropdownLeft + dropdownWidth > viewportWidth - 10) {
- dropdownWidth = viewportWidth - dropdownLeft - 10;
- }
-
- // Ensure dropdown doesn't overflow viewport vertically
- var viewportHeight = $(window).height();
- var scrollTop = $(window).scrollTop();
- var maxHeight = viewportHeight - (dropdownTop - scrollTop) - 20;
- maxHeight = Math.max(maxHeight, 400);
-
- this.$dropdown.css({
- position: 'absolute',
- top: dropdownTop,
- left: dropdownLeft,
- width: dropdownWidth,
- maxHeight: maxHeight,
- zIndex: 10000
+ self.updateBlockStatus($block);
});
- // Show the dropdown
- this.$dropdown.addClass('show');
+ // Update serialized data
+ this.serializeAllBlocks();
+
+ // Update tab badges and header count
+ this.updateTabBadges();
+
+ // Also update header total count immediately (since all cleared)
+ this.updateHeaderTotalCount();
+ },
+
+ switchToBlock: function(blockType) {
+ // Update tabs
+ 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('.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('
'); }
+ else { $badge.html('
'); }
+ $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('
' + chipCount + ''); }
+ else { $badge.html('
' + 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 = '
' + 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;
+
+ var blockTypesWithData = [];
+ 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 groupCount = $block.find('.selection-group').length;
+ var $badge = $tab.find('.tab-badge');
+
+ if (groupCount > 0) {
+ // 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('
');
+ }
+ $tab.addClass('has-data');
+ blockTypesWithData.push(blockType);
+ } else if ($block.hasClass('custom-block')) {
+ var hasCustomValue = false;
+ $block.find('.custom-block-content').find('input, textarea, select').each(function() {
+ if ($(this).val() && $(this).val().trim() !== '') {
+ hasCustomValue = true;
+ return false;
+ }
+ });
+ if (hasCustomValue) {
+ if (!$badge.length) { $tab.append('
'); }
+ else { $badge.removeClass('loading').html('
'); }
+ $tab.addClass('has-data');
+ } else {
+ $badge.remove();
+ $tab.removeClass('has-data');
+ }
+ } else {
+ $badge.remove();
+ $tab.removeClass('has-data');
+ }
+ });
+
+ this.updateTargetSwitchState();
+
+ if (blockTypesWithData.length > 0) {
+ this.fetchAllCounts(blockTypesWithData);
+ }
+ },
+
+ updateTargetSwitchState: function() {
+ var $switch = this.$wrapper.find('.prestashop-switch');
+ if (!$switch.length) {
+ return;
+ }
+
+ // Check if any block has data
+ var hasData = false;
+ this.$wrapper.find('.es-block').each(function() {
+ if ($(this).find('.selection-group').length > 0) {
+ hasData = true;
+ return false; // break
+ }
+ });
+
+ // Update switch: value="1" is "Everyone/All/None", value="0" is "Specific/Selected"
+ if (hasData) {
+ $switch.find('input[value="0"]').prop('checked', true);
+ } else {
+ $switch.find('input[value="1"]').prop('checked', true);
+ }
+ },
+
+ /**
+ * Fetch counts for all block types in a single bulk AJAX request
+ * @param {Array} blockTypes - Array of block type strings to fetch counts for
+ */
+ fetchAllCounts: function(blockTypes) {
+ var self = this;
+
+ var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
+ var savedData = {};
+ try {
+ savedData = JSON.parse($hiddenInput.val() || '{}');
+ } catch (e) {
+ savedData = {};
+ }
+
+ // Build conditions object for all requested block types
+ var conditions = {};
+ blockTypes.forEach(function(blockType) {
+ var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
+ if (groups.length > 0) {
+ conditions[blockType] = { groups: groups };
+ }
+ });
+
+ // 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 = $('
'); $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) {
+ return;
+ }
+
+ $.ajax({
+ url: this.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'previewEntitySelectorBulk',
+ trait: 'EntitySelector',
+ conditions: JSON.stringify(conditions)
+ },
+ success: function(response) {
+ if (response.success && response.counts) {
+ Object.keys(response.counts).forEach(function(blockType) {
+ var count = response.counts[blockType];
+ var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
+ var $badge = $tab.find('.tab-badge');
+ self._setBadgeCount($badge, count);
+ $tab.data('previewData', { count: count, success: true });
+ });
+
+ // 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('.es-block-tab[data-block-type="' + blockType + '"]');
+ $tab.find('.tab-badge').remove();
+ $tab.removeClass('has-data');
+ }
+ });
+
+ self.updateHeaderTotalCount();
+ } else {
+ console.error('[EntitySelector] Bulk preview failed:', response.error || 'Unknown error');
+ // Remove loading spinners on error
+ blockTypes.forEach(function(blockType) {
+ var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
+ $tab.find('.tab-badge').remove();
+ });
+ }
+ },
+ error: function(xhr, status, error) {
+ console.error('[EntitySelector] Bulk AJAX error:', status, error);
+ // Remove loading spinners on error
+ blockTypes.forEach(function(blockType) {
+ var $tab = self.$wrapper.find('.es-block-tab[data-block-type="' + blockType + '"]');
+ $tab.find('.tab-badge').remove();
+ });
+ }
+ });
+ },
+
+ /**
+ * Fetch count for a single block type (legacy, used for single updates)
+ */
+ fetchProductCount: function(blockType, $tab) {
+ var self = this;
+ var data = {};
+
+ // Read from hidden input (contains full saved data or freshly serialized data)
+ var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
+ var savedData = {};
+ try {
+ savedData = JSON.parse($hiddenInput.val() || '{}');
+ } catch (e) {
+ savedData = {};
+ }
+
+ // Get groups for the requested block type
+ var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
+
+ if (groups.length === 0) {
+ $tab.find('.tab-badge').remove();
+ $tab.removeClass('has-data');
+ $tab.removeData('previewData');
+ return;
+ }
+
+ // Show loading state
+ var $badge = $tab.find('.tab-badge');
+ if (!$badge.length) {
+ $badge = $('
');
+ $tab.append($badge);
+ } else {
+ $badge.addClass('loading').html('
');
+ }
+ $tab.addClass('has-data');
+
+ data[blockType] = { groups: groups };
+
+ $.ajax({
+ url: this.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'previewEntitySelector',
+ trait: 'EntitySelector',
+ conditions: JSON.stringify(data),
+ block_type: blockType,
+ limit: 10
+ },
+ success: function(response) {
+ if (response.success) {
+ var $badge = $tab.find('.tab-badge');
+ $badge.removeClass('loading').html('
' + response.count);
+
+ // Store preview data for popover
+ $tab.data('previewData', response);
+
+ // Update header total count
+ self.updateHeaderTotalCount();
+ } else {
+ console.error('[EntitySelector] Preview failed for', blockType, ':', response.error || 'Unknown error');
+ $tab.find('.tab-badge').remove();
+ }
+ },
+ error: function(xhr, status, error) {
+ console.error('[EntitySelector] AJAX error for', blockType, ':', status, error);
+ $tab.find('.tab-badge').remove();
+ self.updateHeaderTotalCount();
+ }
+ });
+ },
+
+ updateHeaderTotalCount: function() {
+ var self = this;
+ var total = 0;
+
+ // Sum up all tab badge counts
+ this.$wrapper.find('.es-block-tab .tab-badge').each(function() {
+ var $badge = $(this);
+ if (!$badge.hasClass('loading')) {
+ var count = parseInt($badge.text(), 10);
+ if (!isNaN(count)) {
+ total += count;
+ }
+ }
+ });
+
+ var $totalBadge = this.$wrapper.find('.trait-total-count');
+ if (total > 0) {
+ $totalBadge.find('.count-value').text(total);
+ $totalBadge.show();
+ } else {
+ $totalBadge.hide();
+ }
+
+ // Update show-all toggle state
+ this.updateShowAllToggle();
+ },
+
+ updateShowAllToggle: function() {
+ var $toggle = this.$wrapper.find('.trait-show-all-toggle');
+ if (!$toggle.length) return;
+
+ var $checkbox = $toggle.find('.show-all-checkbox');
+ 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);
+ },
+
+ updateBlockStatus: function($block) {
+ var $status = $block.find('.block-status');
+ var blockType = $block.data('blockType');
+ var blockDef = this.config.blocks[blockType] || {};
+ var trans = this.config.trans || {};
+
+ var groups = this.getBlockGroups($block);
+
+ if (groups.length === 0) {
+ var emptyMeansAll = this.config.emptyMeansAll !== false;
+ if (emptyMeansAll) {
+ $status.text((trans.all || 'All') + ' ' + (blockDef.entity_label_plural || 'items'));
+ } else {
+ $status.text(trans.nothing_selected || 'Nothing selected');
+ }
+ } else {
+ $status.text(groups.length + ' ' + (groups.length === 1 ? (trans.group || 'group') : (trans.groups || 'groups')));
+ }
+ },
+
+ getEmptyStateText: function($block) {
+ var blockType = $block.data('blockType');
+ var blockMode = $block.data('mode') || 'multi';
+ var blockDef = this.config.blocks[blockType] || {};
+ var trans = this.config.trans || {};
+ var emptyMeansAll = this.config.emptyMeansAll !== false;
+
+ if (blockMode === 'single') {
+ return trans.no_item_selected || 'No item selected';
+ }
+
+ if (emptyMeansAll) {
+ return (trans.all || 'All') + ' ' + (blockDef.entity_label_plural || 'items') + ' ' + (trans.included || 'included');
+ }
+
+ return trans.nothing_selected || 'Nothing selected';
+ },
+
+ serializeGroup: function($group, blockType) {
+ var self = this;
+
+ // Include
+ var includeMethod = $group.find('.include-method-select').val() || 'all';
+ var $includePicker = $group.find('.include-picker');
+ var includeValues = this.getPickerValues($includePicker);
+
+ // Excludes (multiple rows)
+ var excludes = [];
+ var $excludesSection = $group.find('.group-excludes.has-excludes');
+ if ($excludesSection.length) {
+ $group.find('.exclude-row').each(function() {
+ var $row = $(this);
+ var excludeMethod = $row.find('.exclude-method-select').val() || null;
+ var $excludePicker = $row.find('.exclude-picker');
+ var excludeValues = self.getPickerValues($excludePicker);
+
+ if (excludeMethod && excludeValues && (Array.isArray(excludeValues) ? excludeValues.length > 0 : true)) {
+ excludes.push({
+ method: excludeMethod,
+ values: excludeValues
+ });
+ }
+ });
+ }
+
+ var groupData = {
+ include: {
+ method: includeMethod,
+ values: includeValues
+ }
+ };
+
+ if (excludes.length > 0) {
+ groupData.excludes = excludes;
+ }
+
+ // Add modifiers if present
+ var modifiers = this.getGroupModifiers($group);
+ if (modifiers.limit || modifiers.sort_by) {
+ groupData.modifiers = modifiers;
+ }
+
+ return groupData;
+ },
+
+ serializeAllBlocks: function($changedRow) {
+ var self = this;
+ var data = {};
+
+ this.$wrapper.find('.es-block').each(function() {
+ var $block = $(this);
+ var blockType = $block.data('blockType');
+ var groups = self.getBlockGroups($block);
+
+ if (groups.length > 0) {
+ data[blockType] = { groups: groups };
+ }
+
+ self.updateBlockStatus($block);
+ });
+
+ var $input = this.$wrapper.find('input[name="' + this.config.name + '"]');
+ var jsonData = JSON.stringify(data);
+
+ // Skip if data hasn't changed since last serialization
+ if (this._lastSerializedData === jsonData) return;
+ this._lastSerializedData = jsonData;
+
+ $input.val(jsonData);
+
+ // Single mode: update badges from chip count directly, no AJAX
+ if (this.config.mode === 'single') {
+ this._updateSingleModeBadges();
+ return;
+ }
+
+ // 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) {
+ self.updateConditionCount($changedRow);
+ var $group = $changedRow.closest('.selection-group');
+ if ($group.length) {
+ self.updateGroupTotalCount($group);
+ }
+ } else {
+ self.updateAllConditionCounts();
+ }
+ }, 300);
+ },
+
+ getBlockGroups: function($block) {
+ var self = this;
+ var groups = [];
+
+ $block.find('.selection-group').each(function() {
+ var $group = $(this);
+
+ // Include
+ var includeMethod = $group.find('.include-method-select').val() || 'all';
+ var $includePicker = $group.find('.include-picker');
+ var includeValues = self.getPickerValues($includePicker);
+
+ // Skip groups with invalid include conditions (e.g., "specific products" with none selected)
+ if (!self.isConditionValid(includeMethod, includeValues, $includePicker)) {
+ return true; // continue to next group
+ }
+
+ // Excludes (multiple rows) - only include valid ones
+ var excludes = [];
+ var $excludesSection = $group.find('.group-excludes.has-excludes');
+ if ($excludesSection.length) {
+ $group.find('.exclude-row').each(function() {
+ var $row = $(this);
+ var excludeMethod = $row.find('.exclude-method-select').val() || null;
+ var $excludePicker = $row.find('.exclude-picker');
+ var excludeValues = self.getPickerValues($excludePicker);
+
+ // Only include valid exclude conditions
+ if (excludeMethod && self.isConditionValid(excludeMethod, excludeValues, $excludePicker)) {
+ excludes.push({
+ method: excludeMethod,
+ values: excludeValues
+ });
+ }
+ });
+ }
+
+ var groupData = {
+ include: {
+ method: includeMethod,
+ values: includeValues
+ }
+ };
+
+ // Group name (optional, for organizational purposes)
+ var groupName = $.trim($group.attr('data-group-name') || '');
+ if (groupName) {
+ groupData.name = groupName;
+ }
+
+ if (excludes.length > 0) {
+ groupData.excludes = excludes;
+ }
+
+ // Group-level modifiers
+ var modifiers = self.getGroupModifiers($group);
+ if (modifiers.limit || modifiers.sort_by) {
+ groupData.modifiers = modifiers;
+ }
+
+ groups.push(groupData);
+ });
+
+ return groups;
+ },
+
+ getGroupModifiers: function($group) {
+ var limit = $group.find('.group-modifier-limit').val();
+ var sortBy = $group.find('.group-modifier-sort').val() || 'sales';
+ var $sortDirBtn = $group.find('.group-modifiers .btn-sort-dir');
+ var sortDir = $sortDirBtn.data('dir') || 'DESC';
+
+ return {
+ limit: limit ? parseInt(limit, 10) : null,
+ sort_by: sortBy || null,
+ sort_dir: sortDir || 'DESC'
+ };
+ },
+
+ getPickerValues: function($picker) {
+ var valueType = $picker.attr('data-value-type') || 'entity_search';
+ var values = [];
+
+ switch (valueType) {
+ case 'entity_search':
+ $picker.find('.entity-chip').each(function() {
+ var id = $(this).data('id');
+ values.push(isNaN(id) ? id : Number(id));
+ });
+ break;
+
+ case 'pattern':
+ values = this.getPatternTags($picker);
+ // Also include draft pattern if it has content (not yet added as tag)
+ var $draftInput = $picker.find('.draft-tag .pattern-input');
+ var draftPattern = $.trim($draftInput.val());
+ if (draftPattern) {
+ var draftCaseSensitive = $draftInput.closest('.draft-tag').attr('data-case-sensitive') === '1';
+ values.push({
+ pattern: draftPattern,
+ caseSensitive: draftCaseSensitive
+ });
+ }
+ break;
+
+ case 'numeric_range':
+ var min = $picker.find('.range-min-input').val();
+ var max = $picker.find('.range-max-input').val();
+ if (min !== '' || max !== '') {
+ values = {
+ min: min !== '' ? parseFloat(min) : null,
+ max: max !== '' ? parseFloat(max) : null
+ };
+ }
+ break;
+
+ case 'date_range':
+ var from = $picker.find('.date-from-input').val();
+ var to = $picker.find('.date-to-input').val();
+ if (from || to) {
+ values = {
+ from: from || null,
+ to: to || null
+ };
+ }
+ break;
+
+ case 'select':
+ var selectVal = $picker.find('.select-value-input').val();
+ if (selectVal) {
+ values = [selectVal];
+ }
+ break;
+
+ case 'boolean':
+ values = [true];
+ break;
+
+ case 'multi_numeric_range':
+ var ranges = [];
+ $picker.find('.range-chip').each(function() {
+ var $chip = $(this);
+ var minVal = $chip.data('min');
+ var maxVal = $chip.data('max');
+ ranges.push({
+ min: minVal !== '' && minVal !== undefined ? parseFloat(minVal) : null,
+ max: maxVal !== '' && maxVal !== undefined ? parseFloat(maxVal) : null
+ });
+ });
+ if (ranges.length > 0) {
+ values = ranges;
+ }
+ break;
+
+ case 'multi_select_tiles':
+ $picker.find('.tile-option.selected').each(function() {
+ values.push($(this).data('value'));
+ });
+ break;
+
+ case 'combination_attributes':
+ // Returns object: { mode: 'products'|'combinations', attributes: { groupId: [valueId1, valueId2], ... } }
+ var combAttrs = {};
+ $picker.find('.comb-attr-value.selected').each(function() {
+ var groupId = $(this).data('groupId').toString();
+ var valueId = $(this).data('valueId');
+ if (!combAttrs[groupId]) {
+ combAttrs[groupId] = [];
+ }
+ combAttrs[groupId].push(valueId);
+ });
+ if (Object.keys(combAttrs).length > 0) {
+ // Get mode: from radio if toggle exists, otherwise from config
+ var $combPicker = $picker.find('.combination-attributes-picker');
+ var configMode = $combPicker.data('combinationMode') || this.config.combinationMode || 'products';
+ var combMode;
+ if (configMode === 'toggle') {
+ combMode = $picker.find('.comb-mode-radio:checked').val() || 'products';
+ } else {
+ combMode = configMode;
+ }
+ values = {
+ mode: combMode,
+ attributes: combAttrs
+ };
+ }
+ break;
+ }
+
+ return values;
+ },
+
+ isConditionValid: function(method, values, $picker) {
+ // 'all' method never needs values
+ if (method === 'all') {
+ return true;
+ }
+
+ // Boolean methods are always valid (the value is implicit true)
+ var valueType = $picker.attr('data-value-type') || 'entity_search';
+ if (valueType === 'boolean') {
+ return true;
+ }
+
+ // For other methods, check if values are meaningful
+ if (Array.isArray(values)) {
+ return values.length > 0;
+ }
+
+ // For object values (ranges, combination_attributes), check if meaningful
+ if (typeof values === 'object' && values !== null) {
+ // Special handling for combination_attributes: { mode, attributes }
+ if (valueType === 'combination_attributes' && values.attributes !== undefined) {
+ return Object.keys(values.attributes).length > 0;
+ }
+ // For ranges and other objects, check if at least one bound is set
+ return Object.keys(values).some(function(key) {
+ return values[key] !== null && values[key] !== '';
+ });
+ }
+
+ return false;
+ },
+
+ /**
+ * Update all condition counts using a single bulk AJAX request
+ */
+ updateAllConditionCounts: function() {
+ var self = this;
+ var conditions = {};
+ var conditionElements = {};
+ var conditionIndex = 0;
+
+ // Collect all conditions from all active groups
+ this.$wrapper.find('.es-block.active .selection-group').each(function() {
+ var $group = $(this);
+ var $block = $group.closest('.es-block');
+ var blockType = $block.data('blockType') || 'products';
+
+ // Process include row
+ var $include = $group.find('.group-include');
+ if ($include.length) {
+ var includeData = self.getConditionData($include, blockType);
+ if (includeData) {
+ var id = 'c' + conditionIndex++;
+ conditions[id] = includeData.condition;
+ conditionElements[id] = includeData.$countEl;
+ }
+ }
+
+ // Process exclude rows
+ $group.find('.exclude-row').each(function() {
+ var excludeData = self.getConditionData($(this), blockType);
+ if (excludeData) {
+ var id = 'c' + conditionIndex++;
+ conditions[id] = excludeData.condition;
+ conditionElements[id] = excludeData.$countEl;
+ }
+ });
+ });
+
+ // If no conditions, nothing to do
+ if (Object.keys(conditions).length === 0) {
+ return;
+ }
+
+ // Make single bulk AJAX request
+ $.ajax({
+ url: this.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'countConditionMatchesBulk',
+ trait: 'EntitySelector',
+ conditions: JSON.stringify(conditions)
+ },
+ success: function(response) {
+ if (response && response.success && response.counts) {
+ // Update each count element with its result
+ Object.keys(response.counts).forEach(function(id) {
+ var count = response.counts[id] || 0;
+ var $countEl = conditionElements[id];
+ if ($countEl && $countEl.length) {
+ $countEl.removeClass('no-matches clickable');
+ if (count === 0) {
+ $countEl.find('.preview-count').text(count);
+ self._setBadgeCount($countEl, 0);
+ } else {
+ self._setBadgeCount($countEl, count);
+ }
+ }
+ });
+ }
+ // Note: Group totals are updated on-demand when user interacts, not on initial load
+ },
+ error: function() {
+ // Hide all count elements on error
+ Object.keys(conditionElements).forEach(function(id) {
+ var $countEl = conditionElements[id];
+ if ($countEl && $countEl.length) {
+ self._setBadgeCount($countEl, 0);
+ }
+ });
+ }
+ });
+ },
+
+ /**
+ * Extract condition data from a row for bulk counting
+ */
+ getConditionData: function($row, blockType) {
+ var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
+ if (!$countEl.length) return null;
+
+ var isExclude = $row.hasClass('exclude-row');
+ var $methodSelect = isExclude
+ ? $row.find('.exclude-method-select')
+ : $row.find('.include-method-select');
+
+ var method = $methodSelect.val();
+ if (!method) {
+ self._setBadgeCount($countEl, 0);
+ return null;
+ }
+
+ var $picker = isExclude
+ ? $row.find('.exclude-picker')
+ : $row.find('.include-picker');
+ var valueType = $picker.data('valueType') || $picker.attr('data-value-type') || 'none';
+
+ // Special case: "All countries" method - needs separate handling for holidays
+ if (valueType === 'none' && blockType === 'countries' && method === 'all') {
+ // Trigger separate update for this special case (uses nested AJAX)
+ var self = this;
+ setTimeout(function() {
+ self.updateConditionCount($row, blockType);
+ }, 0);
+ return null; // Skip bulk processing, handled separately
+ }
+
+ // 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') {
+ var self = this;
+ setTimeout(function() {
+ self.updateConditionCount($row, blockType);
+ }, 0);
+ return null; // Skip bulk processing, handled separately
+ }
+
+ // "All" type methods (valueType === 'none') — store conditionData for preview, count comes from fetchAllCounts
+ if (valueType === 'none') {
+ $countEl.data('conditionData', {
+ method: method,
+ values: [],
+ blockType: blockType,
+ isExclude: isExclude
+ });
+ return null;
+ }
+
+ var values = this.getPickerValues($picker, valueType);
+
+ // Don't count if no values (except for boolean methods)
+ var hasNoValues = !values ||
+ (Array.isArray(values) && values.length === 0) ||
+ (typeof values === 'object' && !Array.isArray(values) && (
+ (valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
+ (valueType !== 'combination_attributes' && Object.keys(values).length === 0)
+ ));
+ if (valueType !== 'boolean' && hasNoValues) {
+ self._setBadgeCount($countEl, 0);
+ return null;
+ }
+
+ // 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', {
+ method: method,
+ values: values,
+ blockType: blockType,
+ isExclude: isExclude
+ });
+
+ return {
+ condition: {
+ method: method,
+ values: values,
+ block_type: blockType
+ },
+ $countEl: $countEl
+ };
+ },
+
+ updateGroupCounts: function($group) {
+ var self = this;
+ var $block = $group.closest('.es-block');
+ var blockType = $block.data('blockType') || 'products';
+
+ // Update include count
+ var $include = $group.find('.group-include');
+ if ($include.length) {
+ this.updateConditionCount($include, blockType);
+ }
+
+ // Update each exclude row count
+ $group.find('.exclude-row').each(function() {
+ self.updateConditionCount($(this), blockType);
+ });
+
+ // Update group total count (include - excludes)
+ this.updateGroupTotalCount($group);
+ },
+
+ /**
+ * Update a single condition count (used for individual updates after user changes)
+ */
+ updateConditionCount: function($row, blockType) {
+ var self = this;
+
+ var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
+ if (!$countEl.length) {
+ return;
+ }
+
+ var isExclude = $row.hasClass('exclude-row');
+ var $methodSelect = isExclude
+ ? $row.find('.exclude-method-select')
+ : $row.find('.include-method-select');
+
+ var method = $methodSelect.val();
+ if (!method) {
+ self._setBadgeCount($countEl, 0);
+ return;
+ }
+
+ var $picker = isExclude
+ ? $row.find('.exclude-picker')
+ : $row.find('.include-picker');
+
+ var valueType = $picker.data('valueType') || 'none';
+ var searchEntity = $picker.attr('data-search-entity') || '';
+
+ // Get the block type to check if this is a countries block
+ if (!blockType) {
+ var $block = $row.closest('.es-block');
+ blockType = $block.data('blockType') || 'products';
+ }
+
+ // Special case: "All countries" method - fetch holidays for all countries
+ if (valueType === 'none' && blockType === 'countries' && method === 'all') {
+ if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
+
+ // First fetch all active country IDs, then get holidays
+ $.ajax({
+ url: self.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'searchTargetEntities',
+ trait: 'EntitySelector',
+ entity_type: 'countries',
+ query: '',
+ limit: 500
+ },
+ success: function(response) {
+ var items = response.results || response.items || [];
+ if (response && response.success && items.length > 0) {
+ var allCountryIds = items.map(function(item) { return item.id; });
+
+ // Store condition data for click handler
+ $countEl.data('conditionData', {
+ method: method,
+ values: allCountryIds,
+ blockType: blockType,
+ isExclude: isExclude,
+ isCountryHolidays: true,
+ countryIds: allCountryIds,
+ isAllCountries: true
+ });
+
+ // Now fetch holiday count
+ $.ajax({
+ url: self.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'getHolidaysForCountries',
+ trait: 'EntitySelector',
+ country_ids: allCountryIds.join(','),
+ count_only: 1
+ },
+ success: function(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);
+ self._setBadgeCount($countEl, 0);
+ } else {
+ self._setBadgeCount($countEl, count);
+ }
+ $countEl.data('countriesInfo', holidayResponse.countries || []);
+ } else {
+ self._setBadgeCount($countEl, 0);
+ }
+ },
+ error: function() {
+ self._setBadgeCount($countEl, 0);
+ }
+ });
+ } else {
+ self._setBadgeCount($countEl, 0);
+ }
+ },
+ error: function() {
+ self._setBadgeCount($countEl, 0);
+ }
+ });
+ return;
+ }
+
+ // "All" type methods (valueType === 'none') — store conditionData for preview
+ if (valueType === 'none') {
+ $countEl.data('conditionData', {
+ method: method,
+ values: [],
+ blockType: blockType,
+ isExclude: isExclude
+ });
+ return;
+ }
+
+ var values = this.getPickerValues($picker, valueType);
+
+ var hasNoValues = !values ||
+ (Array.isArray(values) && values.length === 0) ||
+ (typeof values === 'object' && !Array.isArray(values) && (
+ (valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
+ (valueType !== 'combination_attributes' && Object.keys(values).length === 0)
+ ));
+ if (valueType !== 'boolean' && hasNoValues) {
+ self._setBadgeCount($countEl, 0);
+ return;
+ }
+
+ if (!blockType) {
+ 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');
+
+ if ($countEl.hasClass('no-matches')) { $countEl.addClass('loading-count'); } $countEl.show();
+
+ // For countries, fetch holiday count
+ if (isCountrySelection && Array.isArray(values) && values.length > 0) {
+ $countEl.data('conditionData', {
+ method: method,
+ values: values,
+ blockType: blockType,
+ isExclude: isExclude,
+ isCountryHolidays: true,
+ countryIds: values
+ });
+
+ $.ajax({
+ url: self.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'getHolidaysForCountries',
+ trait: 'EntitySelector',
+ country_ids: values.join(','),
+ count_only: 1
+ },
+ success: function(response) {
+ if (response && response.success) {
+ var count = response.total_count || 0;
+ $countEl.removeClass('no-matches clickable');
+ $countEl.addClass('country-holidays');
+ if (count === 0) {
+ $countEl.find('.preview-count').text(count);
+ self._setBadgeCount($countEl, 0);
+ } else {
+ self._setBadgeCount($countEl, count);
+ }
+ // Store countries info for popover
+ $countEl.data('countriesInfo', response.countries || []);
+ } else {
+ self._setBadgeCount($countEl, 0);
+ }
+ },
+ error: function() {
+ self._setBadgeCount($countEl, 0);
+ }
+ });
+ return;
+ }
+
+ // Default: count entities
+ $countEl.data('conditionData', {
+ method: method,
+ values: values,
+ blockType: blockType,
+ isExclude: isExclude
+ });
+
+ $.ajax({
+ url: this.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'countConditionMatches',
+ trait: 'EntitySelector',
+ method: method,
+ values: JSON.stringify(values),
+ block_type: blockType
+ },
+ 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);
+ self._setBadgeCount($countEl, 0);
+ } else {
+ self._setBadgeCount($countEl, count);
+ }
+ } else {
+ self._setBadgeCount($countEl, 0);
+ }
+ },
+ error: function() {
+ self._setBadgeCount($countEl, 0);
+ }
+ });
+ },
+
+ updateGroupTotalCount: function($group) {
+ var self = this;
+ 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');
+
+ // Build group data for AJAX
+ var groupData = this.serializeGroup($group, blockType);
+
+ // Check if include has valid data
+ if (!groupData.include || !groupData.include.method) {
+ $badge.hide();
+ $limitInput.attr('placeholder', '–');
+ return;
+ }
+
+ // Show loading
+ if (!$badge.hasClass('clickable')) { $badge.addClass('loading-count'); } $badge.show();
+
+ $.ajax({
+ url: this.config.ajaxUrl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ ajax: 1,
+ action: 'countGroupItems',
+ trait: 'EntitySelector',
+ group_data: JSON.stringify(groupData),
+ block_type: blockType
+ },
+ success: function(response) {
+ if (response && response.success) {
+ var finalCount = response.final_count || 0;
+ var excludeCount = response.exclude_count || 0;
+
+ var badgeHtml = '
' + finalCount;
+ if (excludeCount > 0) {
+ badgeHtml += '
(-' + excludeCount + ')';
+ }
+ if ($badge.html() !== badgeHtml) {
+ $badge.html(badgeHtml);
+ }
+ self._setBadgeCount($badge, finalCount);
+
+ // Store group data on badge for preview popover
+ $badge.data('groupData', groupData);
+ $badge.data('blockType', blockType);
+ $badge.data('finalCount', finalCount);
+
+ // Update limit placeholder with the count
+ $limitInput.attr('placeholder', finalCount);
+
+ // Also update the group-preview-badge count (apply limit if set)
+ var $previewBadge = $group.find('.group-preview-badge .preview-count');
+ if ($previewBadge.length) {
+ var limit = parseInt($limitInput.val(), 10);
+ var displayCount = (limit > 0 && limit < finalCount) ? limit : finalCount;
+ $previewBadge.text(displayCount);
+ }
+ } else {
+ self._setBadgeCount($badge, 0);
+ $limitInput.attr('placeholder', '–');
+ }
+ },
+ error: function() {
+ $badge.hide();
+ $limitInput.attr('placeholder', '–');
+ }
+ });
+ },
+
+ // Exclude row management
+ addFirstExcludeRow: function($group, $block) {
+ var $excludesDiv = $group.find('.group-excludes');
+ var trans = this.config.trans || {};
+
+ // Build the full excludes structure with first row
+ var html = '
';
+ html += ' ' + (trans.except || 'EXCEPT') + '';
+ html += '
';
+
+ html += '
';
+ html += this.buildExcludeRowHtml($block, 0);
+ html += '
';
+
+ html += '
';
+
+ $excludesDiv.addClass('has-excludes').html(html);
+
+ // Enhance the first exclude method select with styled dropdown
+ var $firstRow = $excludesDiv.find('.exclude-row[data-exclude-index="0"]');
+ var $firstSelect = $firstRow.find('.exclude-method-select');
+ this.enhanceMethodSelect($firstSelect);
+
+ // Update method info placeholder for initial selection
+ var blockType = $block.data('blockType');
+ var initialMethod = $firstSelect.val();
+ this.updateMethodInfoPlaceholder($firstRow.find('.method-selector-wrapper'), initialMethod, blockType);
+
+ this.updateMethodSelectorLock($group, true);
+ this.serializeAllBlocks();
+ },
+
+ addExcludeRow: function($group, $block) {
+ var $container = $group.find('.exclude-rows-container');
+
+ // Get next exclude index
+ var maxIndex = -1;
+ $container.find('.exclude-row').each(function() {
+ var idx = parseInt($(this).data('excludeIndex'), 10);
+ if (idx > maxIndex) maxIndex = idx;
+ });
+ var excludeIndex = maxIndex + 1;
+
+ var html = this.buildExcludeRowHtml($block, excludeIndex);
+ $container.append(html);
+
+ // Enhance the exclude method select with styled dropdown
+ var $newRow = $container.find('.exclude-row[data-exclude-index="' + excludeIndex + '"]');
+ var $newSelect = $newRow.find('.exclude-method-select');
+ this.enhanceMethodSelect($newSelect);
+
+ // Update method info placeholder for initial selection
+ var blockType = $block.data('blockType');
+ var initialMethod = $newSelect.val();
+ this.updateMethodInfoPlaceholder($newRow.find('.method-selector-wrapper'), initialMethod, blockType);
+
+ this.serializeAllBlocks();
+ },
+
+ buildExcludeRowHtml: function($block, excludeIndex) {
+ var blockType = $block.data('blockType');
+ var blockDef = this.config.blocks[blockType] || {};
+ var methods = blockDef.selection_methods || {};
+ var trans = this.config.trans || {};
+
+ // Build exclude method options with optgroups (no "all")
+ var excludeMethodOptions = this.buildMethodOptions(methods, true);
+
+ // Find first non-all method for default search entity
+ var firstSearchEntity = blockType;
+ var firstValueType = 'entity_search';
+ $.each(methods, function(methodKey, methodDef) {
+ if (methodKey === 'all') return true;
+ firstSearchEntity = methodDef.search_entity || blockType;
+ firstValueType = methodDef.value_type || 'entity_search';
+ return false; // break
+ });
+
+ var html = '
';
+
+ // Header row with method select wrapped in method-selector-wrapper (same as include)
+ html += '';
+
+ // Value picker based on first method's value type
+ html += this.buildValuePickerHtml('exclude', firstValueType, firstSearchEntity, methods);
+
+ html += '
';
+
+ return html;
+ },
+
+ removeExcludeRow: function($excludeRow, $group, $block) {
+ var $container = $group.find('.exclude-rows-container');
+ var trans = this.config.trans || {};
+
+ $excludeRow.remove();
+
+ // Check if there are remaining exclude rows
+ var remainingRows = $container.find('.exclude-row').length;
+
+ if (remainingRows === 0) {
+ // Remove entire excludes section and show "Add exceptions" button
+ var $excludesDiv = $group.find('.group-excludes');
+ $excludesDiv.removeClass('has-excludes').html(
+ '
'
+ );
+ // Unlock the method selector since no excludes exist
+ this.updateMethodSelectorLock($group, false);
+ }
+
+ this.serializeAllBlocks();
+ },
+
+ // Method options building
+ buildMethodOptions: function(methods, excludeAll) {
+ var self = this;
+ var trans = this.config.trans || {};
+ var html = '';
+
+ // Group labels
+ var groupLabels = {
+ 'select_by': trans.select_by || 'Select by...',
+ 'filter_by': trans.filter_by || 'Filter by...'
+ };
+
+ // Separate methods by group
+ var grouped = {};
+ var ungrouped = {};
+
+ $.each(methods, function(methodKey, methodDef) {
+ if (excludeAll && methodKey === 'all') return true; // skip
+
+ var group = methodDef.group || '';
+ if (group) {
+ if (!grouped[group]) {
+ grouped[group] = {};
+ }
+ grouped[group][methodKey] = methodDef;
+ } else {
+ ungrouped[methodKey] = methodDef;
+ }
+ });
+
+ // Render ungrouped options first
+ $.each(ungrouped, function(methodKey, methodDef) {
+ html += self.buildMethodOption(methodKey, methodDef);
+ });
+
+ // Render grouped options with optgroups
+ $.each(grouped, function(groupKey, groupMethods) {
+ var groupLabel = groupLabels[groupKey] || groupKey.replace(/_/g, ' ');
+ html += '
';
+ });
+
+ return html;
+ },
+
+ buildMethodOption: function(methodKey, methodDef) {
+ var html = '
';
+ return html;
+ },
+
+ buildValuePickerHtml: function(section, valueType, searchEntity, methods) {
+ var trans = this.config.trans || {};
+ var pickerClass = section + '-picker';
+ var chipsClass = section + '-chips';
+ var dataClass = section + '-values-data';
+ var html = '';
+
+ if (valueType === 'none') {
+ html = '
';
+ html += '';
+ html += '
';
+ return html;
+ }
+
+ html = '
';
+
+ switch (valueType) {
+ case 'entity_search':
+ var noItemsText = trans.no_items_selected || 'No items selected - use search below';
+ html += '
';
+ html += '
';
+ html += '
' + this.escapeHtml(noItemsText) + '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '';
+ html += '';
+ html += '';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'pattern':
+ // Build tooltip content for data-details attribute
+ var tooltipContent = '
' + this.escapeHtml(trans.pattern_help_title || 'Pattern Syntax') + '';
+ tooltipContent += '
';
+ tooltipContent += '
* ' + this.escapeHtml(trans.pattern_help_wildcard || 'any text (wildcard)') + '
';
+ tooltipContent += '
{number} ' + this.escapeHtml(trans.pattern_help_number || 'any number (e.g. 100, 250)') + '
';
+ tooltipContent += '
{letter} ' + this.escapeHtml(trans.pattern_help_letter || 'single letter (A-Z)') + '
';
+ tooltipContent += '
';
+ tooltipContent += '
';
+ tooltipContent += '
' + this.escapeHtml(trans.pattern_help_examples || 'Examples:') + '';
+ tooltipContent += '
*cotton* ' + this.escapeHtml(trans.pattern_example_1 || 'contains "cotton"') + '
';
+ tooltipContent += '
iPhone {number} Pro* ' + this.escapeHtml(trans.pattern_example_2 || 'matches "iPhone 15 Pro Max"') + '
';
+ tooltipContent += '
Size {letter} ' + this.escapeHtml(trans.pattern_example_3 || 'matches "Size M", "Size L"') + '
';
+ tooltipContent += '
';
+
+ var noPatternText = trans.no_patterns || 'No patterns - press Enter to add';
+ html += '
';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'numeric_range':
+ html += '
';
+ html += '';
+ html += '-';
+ html += '';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'multi_numeric_range':
+ html += '
';
+ html += '
';
+ break;
+
+ case 'multi_select_tiles':
+ html += '
';
+ // Tiles will be populated based on method options
+ html += '
';
+ html += '
';
+ break;
+
+ case 'date_range':
+ html += '
';
+ html += '';
+ html += '-';
+ html += '';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'select':
+ html += '
';
+ html += '';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'boolean':
+ html += '
';
+ html += '' + this.escapeHtml(trans.yes || 'Yes') + '';
+ html += '
';
+ html += '
';
+ break;
+
+ case 'combination_attributes':
+ // Build tooltip content
+ var combTooltip = '
' + this.escapeHtml(trans.combination_help_title || 'Combination Targeting') + '';
+ combTooltip += '
';
+ combTooltip += '
' + this.escapeHtml(trans.combination_help_desc || 'Select attributes to target specific product combinations.') + '
';
+ combTooltip += '
' + this.escapeHtml(trans.combination_help_logic || 'Logic:') + '
';
+ combTooltip += '
';
+ combTooltip += '- ' + this.escapeHtml(trans.combination_help_within || 'Within group: OR (Red OR Blue)') + '
';
+ combTooltip += '- ' + this.escapeHtml(trans.combination_help_between || 'Between groups: AND (Color AND Size)') + '
';
+ combTooltip += '
';
+ combTooltip += '
';
+
+ // Combination mode from config: 'products', 'combinations', or 'toggle'
+ var combMode = this.config.combinationMode || 'products';
+ var showModeToggle = (combMode === 'toggle');
+ var defaultMode = showModeToggle ? 'products' : combMode;
+
+ html += '
';
+ // Store mode along with attributes: { mode: 'products'|'combinations', attributes: { groupId: [valueIds] } }
+ html += '
';
+ break;
+
+ default:
+ html += '
';
+ break;
+ }
+
+ html += '
';
+ return html;
+ },
+
+ // Sort options
+ getSortOptionsArray: function(blockType) {
+ var trans = this.config.trans || {};
+
+ switch (blockType) {
+ case 'products':
+ return [
+ { value: 'sales', label: trans.sort_bestsellers || 'Best sellers' },
+ { value: 'date_add', label: trans.sort_newest || 'Newest' },
+ { value: 'price', label: trans.sort_price || 'Price' },
+ { value: 'name', label: trans.sort_name || 'Name' },
+ { value: 'position', label: trans.sort_position || 'Position' },
+ { value: 'quantity', label: trans.sort_stock || 'Stock quantity' },
+ { value: 'random', label: trans.sort_random || 'Random' }
+ ];
+ case 'categories':
+ return [
+ { value: 'name', label: trans.sort_name || 'Name' },
+ { value: 'position', label: trans.sort_position || 'Position' },
+ { value: 'product_count', label: trans.sort_products || 'Products count' },
+ { value: 'date_add', label: trans.sort_newest || 'Newest' }
+ ];
+ default:
+ return [
+ { value: 'name', label: trans.sort_name || 'Name' },
+ { value: 'date_add', label: trans.sort_newest || 'Newest' }
+ ];
+ }
+ },
+
+ getSortIconClass: function(sortBy, sortDir) {
+ var isAsc = (sortDir === 'ASC');
+
+ switch (sortBy) {
+ case 'name':
+ return isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
+ case 'price':
+ case 'quantity':
+ case 'product_count':
+ return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
+ case 'date_add':
+ case 'newest_products':
+ return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
+ case 'sales':
+ case 'total_sales':
+ return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
+ case 'position':
+ return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
+ case 'random':
+ return 'icon-random';
+ default:
+ return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
+ }
+ },
+
+ cycleSortOption: function($btn, blockType) {
+ var sortOptions = this.getSortOptionsArray(blockType);
+ var currentSort = $btn.data('sort') || 'sales';
+ var currentDir = $btn.data('dir') || 'DESC';
+
+ // Find current index
+ var currentIndex = -1;
+ for (var i = 0; i < sortOptions.length; i++) {
+ if (sortOptions[i].value === currentSort) {
+ currentIndex = i;
+ break;
+ }
+ }
+
+ // Cycle: first toggle direction, then move to next sort option
+ var newSort, newDir, newLabel;
+ if (currentDir === 'DESC') {
+ // Toggle to ASC, same sort
+ newSort = currentSort;
+ newDir = 'ASC';
+ } else {
+ // Move to next sort option, reset to DESC
+ var nextIndex = (currentIndex + 1) % sortOptions.length;
+ newSort = sortOptions[nextIndex].value;
+ newDir = 'DESC';
+ }
+
+ // Find label for new sort
+ for (var j = 0; j < sortOptions.length; j++) {
+ if (sortOptions[j].value === newSort) {
+ newLabel = sortOptions[j].label;
+ break;
+ }
+ }
+
+ // Update button
+ $btn.data('sort', newSort);
+ $btn.data('dir', newDir);
+ $btn.attr('data-sort', newSort);
+ $btn.attr('data-dir', newDir);
+ $btn.attr('title', newLabel + ' ' + (newDir === 'DESC' ? '↓' : '↑'));
+ $btn.find('i').attr('class', this.getSortIconClass(newSort, newDir));
+ },
+
+ // Validation
+ validate: function() {
+ var isRequired = this.$wrapper.data('required') === 1 || this.$wrapper.data('required') === '1';
+ if (!isRequired) {
+ return true;
+ }
+
+ // Check if any block has data (groups with selections)
+ var hasData = false;
+ this.$wrapper.find('.es-block').each(function() {
+ if ($(this).find('.selection-group').length > 0) {
+ hasData = true;
+ return false; // break
+ }
+ });
+
+ if (!hasData) {
+ // Show validation error
+ this.showValidationError();
+ return false;
+ }
+
+ // Valid - remove any previous error
+ this.clearValidationError();
+ return true;
+ },
+
+ showValidationError: function() {
+ this.$wrapper.addClass('has-validation-error');
+ var message = this.$wrapper.data('required-message') || 'Please select at least one item';
+
+ // Remove any existing error
+ this.$wrapper.find('.trait-validation-error').remove();
+
+ // Add error message after header
+ var $error = $('
', {
+ class: 'trait-validation-error',
+ html: '
' + message
+ });
+ this.$wrapper.find('.condition-trait-header').after($error);
+
+ // Scroll to error
+ $('html, body').animate({
+ scrollTop: this.$wrapper.offset().top - 100
+ }, 300);
+
+ // Expand the trait if collapsed
+ if (!this.$wrapper.find('.condition-trait-body').hasClass('es-expanded')) {
+ this.$wrapper.find('.condition-trait-body').addClass('es-expanded');
+ this.$wrapper.removeClass('collapsed');
+ }
+ },
+
+ clearValidationError: function() {
+ this.$wrapper.removeClass('has-validation-error');
+ this.$wrapper.find('.trait-validation-error').remove();
}
};
@@ -3078,7 +4636,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;
@@ -3143,7 +4701,7 @@
var html = '';
if (visibleResults.length === 0 && !appendMode) {
- html = '
' + this.esIcon('search') + ' ' + (trans.no_results || 'No results found') + '
';
+ html = '
' + (trans.no_results || 'No results found') + '
';
} else {
visibleResults.forEach(function(item) {
var isSelected = selectedIds.indexOf(String(item.id)) !== -1;
@@ -3158,27 +4716,27 @@
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
html += '>';
- html += '
' + self.esIcon('check') + '';
+ html += '
';
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 += '
' + self.esIcon('flag') + '';
+ html += '
';
} else if (item.image) {
html += '
';
} 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 += '
' + self.esIcon(iconName) + '
';
+ 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 += '
';
}
html += '
';
@@ -3370,10 +4928,10 @@
for (var i = 0; i < history.length; i++) {
var query = history[i];
html += '
';
- html += this.esIcon('schedule');
+ html += '';
html += '' + this.escapeHtml(query) + '';
html += '';
html += '
';
}
@@ -3663,22 +5221,9 @@
})(jQuery);
/**
- * Entity Selector - Filters Module
- * Filter panel, filter state management
- * @partial _filters.js
- *
- * EXTRACTION SOURCE: assets/js/admin/entity-selector.js
- * Lines: 6605-6758 (filter methods)
- *
- * Contains:
- * - clearFilters() - Reset all filters
- * - resetFiltersWithoutSearch() - Reset without triggering search
- * - updateFilterPanelForEntity() - Show/hide filters based on entity type
- * - loadFilterableData() - Load attributes/features for filter panel
- * - renderFilterDropdowns() - Render attribute/feature group toggles
- * - showFilterGroupValues() - Show values for a filter group
- * - hideFilterGroupValues() - Hide filter values row
- * - updateFilterToggleStates() - Update toggle states based on selections
+ * Entity Selector - Dropdown Module
+ * Search dropdown UI creation and positioning
+ * @partial _dropdown.js
*/
(function($) {
@@ -3686,375 +5231,429 @@
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
- window._EntitySelectorMixins.filters = {
+ window._EntitySelectorMixins.dropdown = {
- clearFilters: function() {
- this.refineQuery = '';
- this.refineNegate = false;
- this.filters = {
- inStock: false,
- discounted: false,
- priceMin: null,
- priceMax: null,
- attributes: [],
- features: [],
- productCountMin: null,
- productCountMax: null,
- salesMin: null,
- salesMax: null,
- turnoverMin: null,
- turnoverMax: null,
- depth: null,
- hasProducts: false,
- hasDescription: false,
- hasImage: false,
- activeOnly: true,
- attributeGroup: null,
- featureGroup: null,
- dateAddFrom: null,
- dateAddTo: null,
- lastProductFrom: null,
- lastProductTo: null,
- // Country-specific filters
- hasHolidays: false,
- containsStates: false,
- zone: null
- };
+ createDropdown: function() {
+ this.$wrapper.find('.es-search-dropdown').remove();
+ var trans = this.config.trans || {};
+
+ var html = '
';
+
+ // Header with results count, actions, sort controls, view mode
+ html += ''; // End dropdown-header
+
+ // Filter panel
+ html += '
';
+
+ // Quick filters row (for products)
+ html += '
';
+
+ // Attribute/Feature filter toggles for products
+ html += '
';
+ html += '
' + (trans.attributes || 'Attributes') + ':';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ html += '
';
+ html += '
' + (trans.features || 'Features') + ':';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ // Entity-specific filters: Categories
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ // Entity-specific filters: Manufacturers
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ // Entity-specific filters: Suppliers
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ // Entity-specific filters: Attributes
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += ' ' + (trans.attribute_group || 'Group') + ':';
+ html += '';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ // Entity-specific filters: Features
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += ' ' + (trans.feature_group || 'Group') + ':';
+ html += '';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+
+ // Entity-specific filters: CMS Pages
+ html += '
';
+ html += '';
+ html += '';
+ html += '';
+ html += '
';
+
+ // Entity-specific filters: CMS Categories
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+
+ // Entity-specific filters: Countries
+ html += '
';
+
+ html += '
'; // End filter-panel
+
+ // Results header for list view (product columns)
+ html += '';
+
+ // Results
+ html += '
';
+
+ // Footer - unified load more + actions
+ html += '';
+
+ html += '
';
+
+ this.$dropdown = $(html);
+ $('body').append(this.$dropdown);
+ },
+
+ hideDropdown: function() {
if (this.$dropdown) {
- var trans = this.config.trans || {};
- this.$dropdown.find('.refine-input').val('');
- this.$dropdown.find('.btn-refine-negate').removeClass('active');
- this.$dropdown.find('.filter-in-stock').prop('checked', false);
- this.$dropdown.find('.filter-discounted').prop('checked', false);
- this.$dropdown.find('.filter-price-min, .filter-price-max').val('');
- this.$dropdown.find('.filter-attr-chip, .filter-feat-chip').removeClass('active');
- this.$dropdown.find('.filter-product-count-min, .filter-product-count-max').val('');
- this.$dropdown.find('.filter-sales-min, .filter-sales-max').val('');
- this.$dropdown.find('.filter-depth-select').val('');
- this.$dropdown.find('.filter-has-products').prop('checked', false);
- this.$dropdown.find('.filter-active-only').prop('checked', true);
- // Country filters
- this.$dropdown.find('.filter-has-holidays').prop('checked', false);
- this.$dropdown.find('.filter-contains-states').prop('checked', false);
- this.$dropdown.find('.filter-zone-select').val('');
+ this.$dropdown.removeClass('show');
}
-
- this.refreshSearch();
+ this.activeGroup = null;
},
- resetFiltersWithoutSearch: function() {
- // Same as clearFilters but doesn't trigger search
- // Used when switching entity types
- this.refineQuery = '';
- this.refineNegate = false;
- this.filters = {
- inStock: false,
- discounted: false,
- priceMin: null,
- priceMax: null,
- attributes: [],
- features: [],
- productCountMin: null,
- productCountMax: null,
- salesMin: null,
- salesMax: null,
- turnoverMin: null,
- turnoverMax: null,
- depth: null,
- hasProducts: false,
- hasDescription: false,
- hasImage: false,
- activeOnly: true,
- attributeGroup: null,
- featureGroup: null,
- dateAddFrom: null,
- dateAddTo: null,
- lastProductFrom: null,
- lastProductTo: null,
- // Country-specific filters
- hasHolidays: false,
- containsStates: false,
- zone: null
- };
- },
+ positionDropdown: function($input) {
+ if (!this.$dropdown) return;
- updateFilterPanelForEntity: function(entityType) {
- if (!this.$dropdown) {
- return;
+ var $picker = $input.closest('.value-picker');
+ var $searchBox = $input.closest('.entity-search-box');
+
+ // Get absolute positions (dropdown is appended to body)
+ var searchBoxOffset = $searchBox.offset();
+ var searchBoxHeight = $searchBox.outerHeight();
+ var pickerOffset = $picker.offset();
+ var pickerWidth = $picker.outerWidth();
+
+ // Calculate position relative to document
+ var dropdownTop = searchBoxOffset.top + searchBoxHeight + 4;
+ var dropdownLeft = pickerOffset.left;
+ var dropdownWidth = Math.max(pickerWidth, 400);
+
+ // Ensure dropdown doesn't overflow the viewport horizontally
+ var viewportWidth = $(window).width();
+ if (dropdownLeft + dropdownWidth > viewportWidth - 10) {
+ dropdownWidth = viewportWidth - dropdownLeft - 10;
}
- var $panel = this.$dropdown.find('.filter-panel');
+ // Ensure dropdown doesn't overflow viewport vertically
+ var viewportHeight = $(window).height();
+ var scrollTop = $(window).scrollTop();
+ var maxHeight = viewportHeight - (dropdownTop - scrollTop) - 20;
+ maxHeight = Math.max(maxHeight, 400);
- // Hide all entity-specific filter rows
- $panel.find('.filter-row').hide();
-
- // Show filters for current entity type
- $panel.find('.filter-row[data-entity="' + entityType + '"]').show();
- $panel.find('.filter-row-entity-' + entityType.replace('_', '-')).show();
-
- // Show/hide tree view option based on entity type
- var isCategory = (entityType === 'categories' || entityType === 'cms_categories');
- this.$dropdown.find('.tree-view-option').toggle(isCategory);
-
- // Default to tree view for categories (only if currently on list mode)
- if (isCategory && this.viewMode === 'list') {
- this.viewMode = 'tree';
- this.$dropdown.find('.view-mode-select').val('tree');
- this.$dropdown.removeClass('view-list view-cols-2 view-cols-3 view-cols-4 view-cols-5 view-cols-6 view-cols-7 view-cols-8').addClass('view-tree');
- } else if (!isCategory && this.viewMode === 'tree') {
- // If switching away from categories while in tree mode, switch to list
- this.viewMode = 'list';
- this.$dropdown.find('.view-mode-select').val('list');
- this.$dropdown.removeClass('view-tree view-cols-2 view-cols-3 view-cols-4 view-cols-5 view-cols-6 view-cols-7 view-cols-8').addClass('view-list');
- }
-
- // Load zones for countries filter
- if (entityType === 'countries') {
- this.loadZonesForCountryFilter();
- }
-
- // Update sort options for entity type
- this.updateSortOptionsForEntity(entityType);
- },
-
- /**
- * Show/hide sort options based on entity type
- * Options with data-entities attribute are only shown for matching entities
- */
- updateSortOptionsForEntity: function(entityType) {
- if (!this.$dropdown) {
- return;
- }
-
- var $select = this.$dropdown.find('.sort-field-select');
- var currentValue = $select.val();
- var hasCurrentOption = false;
-
- $select.find('option').each(function() {
- var $option = $(this);
- var entities = $option.data('entities');
-
- // Options without data-entities are universal (always shown)
- if (!entities) {
- $option.show();
- if ($option.val() === currentValue) {
- hasCurrentOption = true;
- }
- return;
- }
-
- // Check if this entity type is in the allowed list
- var allowedEntities = entities.split(',');
- var isAllowed = allowedEntities.indexOf(entityType) !== -1;
-
- $option.toggle(isAllowed);
-
- if (isAllowed && $option.val() === currentValue) {
- hasCurrentOption = true;
- }
+ this.$dropdown.css({
+ position: 'absolute',
+ top: dropdownTop,
+ left: dropdownLeft,
+ width: dropdownWidth,
+ maxHeight: maxHeight,
+ zIndex: 10000
});
- // If current sort field is not available for this entity, reset to 'name'
- if (!hasCurrentOption) {
- $select.val('name');
- this.currentSort.field = 'name';
- }
- },
-
- loadFilterableData: function() {
- var self = this;
-
- if (this.filterableData) {
- this.renderFilterDropdowns();
- return;
- }
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- data: {
- ajax: 1,
- action: 'getTargetFilterableAttributes',
- trait: 'EntitySelector'
- },
- dataType: 'json',
- success: function(response) {
- if (response.success && response.data) {
- self.filterableData = response.data;
- self.renderFilterDropdowns();
- }
- }
- });
- },
-
- renderFilterDropdowns: function() {
- if (!this.$dropdown || !this.filterableData) return;
-
- var self = this;
-
- // Render attribute group toggle buttons
- var $attrContainer = this.$dropdown.find('.filter-attributes-container');
- $attrContainer.empty();
-
- if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
- this.filterableData.attributes.forEach(function(group) {
- var html = '
';
- $attrContainer.append(html);
- });
- this.$dropdown.find('.filter-row-attributes').show();
- }
-
- // Render feature group toggle buttons
- var $featContainer = this.$dropdown.find('.filter-features-container');
- $featContainer.empty();
-
- if (this.filterableData.features && this.filterableData.features.length > 0) {
- this.filterableData.features.forEach(function(group) {
- var html = '
';
- $featContainer.append(html);
- });
- this.$dropdown.find('.filter-row-features').show();
- }
- },
-
- showFilterGroupValues: function(groupId, type) {
- if (!this.filterableData) return;
-
- var self = this;
- var groups = type === 'attribute' ? this.filterableData.attributes : this.filterableData.features;
- var group = groups.find(function(g) { return g.id == groupId; });
-
- if (!group) return;
-
- // Hide all values rows first, then show the correct one
- this.$dropdown.find('.filter-row-values').hide();
-
- // Target the correct values row based on type
- var valuesRowClass = type === 'attribute' ? '.filter-row-attr-values' : '.filter-row-feat-values';
- var $filterRowValues = this.$dropdown.find(valuesRowClass);
- var $valuesContainer = $filterRowValues.find('.filter-values-container');
- $valuesContainer.empty();
-
- // Add group label
- var html = '
' + group.name + ':';
-
- // Add chips
- group.values.forEach(function(val) {
- var isActive = type === 'attribute'
- ? self.filters.attributes.indexOf(val.id) !== -1
- : self.filters.features.indexOf(val.id) !== -1;
- var activeClass = isActive ? ' active' : '';
- var chipClass = type === 'attribute' ? 'filter-attr-chip' : 'filter-feat-chip';
- var colorStyle = val.color ? ' style="--chip-color: ' + val.color + '"' : '';
- var colorClass = val.color ? ' has-color' : '';
-
- html += '
';
- });
-
- $valuesContainer.html(html);
-
- // Add close button as sibling (outside filter-values-container, inside filter-row-values)
- $filterRowValues.find('.btn-close-values').remove();
- $filterRowValues.append('
');
- $filterRowValues.show();
-
- // Scroll into view if needed
- var rowValues = $filterRowValues[0];
- if (rowValues) {
- rowValues.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- }
- },
-
- hideFilterGroupValues: function() {
- this.$dropdown.find('.filter-row-values').hide();
- this.$dropdown.find('.filter-group-toggle').removeClass('active');
- },
-
- updateFilterToggleStates: function() {
- if (!this.$dropdown || !this.filterableData) return;
-
- var self = this;
-
- // Update attribute group toggles
- if (this.filterableData.attributes) {
- this.filterableData.attributes.forEach(function(group) {
- var $toggle = self.$dropdown.find('.filter-group-toggle[data-group-id="' + group.id + '"][data-type="attribute"]');
- var hasActiveInGroup = group.values.some(function(val) {
- return self.filters.attributes.indexOf(val.id) !== -1;
- });
- $toggle.toggleClass('has-selection', hasActiveInGroup);
- });
- }
-
- // Update feature group toggles
- if (this.filterableData.features) {
- this.filterableData.features.forEach(function(group) {
- var $toggle = self.$dropdown.find('.filter-group-toggle[data-group-id="' + group.id + '"][data-type="feature"]');
- var hasActiveInGroup = group.values.some(function(val) {
- return self.filters.features.indexOf(val.id) !== -1;
- });
- $toggle.toggleClass('has-selection', hasActiveInGroup);
- });
- }
- },
-
- /**
- * Load zones for country filter dropdown
- */
- loadZonesForCountryFilter: function() {
- var self = this;
-
- if (this.zonesLoaded || !this.$dropdown) {
- return;
- }
-
- var $select = this.$dropdown.find('.filter-zone-select');
- if (!$select.length) {
- return;
- }
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'getZonesForFilter',
- trait: 'EntitySelector'
- },
- success: function(response) {
- if (response.success && response.zones && response.zones.length > 0) {
- var trans = self.config.trans || {};
- $select.empty();
- $select.append('
');
-
- response.zones.forEach(function(zone) {
- $select.append('
');
- });
-
- self.zonesLoaded = true;
- }
- }
- });
+ // Show the dropdown
+ this.$dropdown.addClass('show');
}
};
@@ -4091,13 +5690,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';
@@ -4109,9 +5710,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';
@@ -4130,6 +5728,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;
@@ -4141,21 +5743,34 @@
}
html += '>';
- // Country: show flag
+ // Icon: flag for countries, image if available, or entity type icon
if (isCountry && data && data.iso_code) {
- html += '
' + this.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '';
+ html += '
) + '.png)
';
} else if (data && data.image) {
html += '
 + ')
';
+ } 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 += '
';
+ }
}
html += '
' + this.escapeHtml(name) + '';
// Country: add holiday preview button
if (isCountry) {
- html += '
';
+ html += '
';
}
- html += '
';
+ html += '
';
html += '';
$chips.append(html);
@@ -4174,19 +5789,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('
' + self.escapeHtml(placeholder) + '');
+ }
+ 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');
@@ -4222,7 +5843,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
@@ -4245,7 +5866,7 @@
'
'
).show();
} else {
@@ -4254,37 +5875,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('
');
+ $wrapper = $chips.closest('.chips-wrapper');
+ $wrapper.prepend('
');
+ $wrapper.append('
');
+ }
+
+ // 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 = '
';
-
- 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'));
+ '
' + (trans.clear || 'Clear') + '' +
+ '';
+ $toolbar.html(toolbarHtml);
// Bind toolbar events
this.bindChipsToolbarEvents($wrapper);
@@ -4423,16 +6051,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
@@ -4477,11 +6102,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({
@@ -4495,9 +6118,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 {
@@ -4537,21 +6158,27 @@
}
html += '>';
- // Country: show flag
+ // Icon: flag, image, or entity type icon
if (isCountry && entity.iso_code) {
- html += '
' + self.esIcon('flag', 'flag-fallback').replace('>', ' style="display:none">') + '';
+ html += '
) + '.png)
';
} else if (entity.image) {
html += '
 + ')
';
+ } else {
+ var bt = $block.data('blockType') || '';
+ var blockDef = self.config.blocks && self.config.blocks[bt];
+ if (blockDef && blockDef.icon) {
+ html += '
';
+ }
}
html += '
' + self.escapeHtml(entity.name) + '';
// Country: add holiday preview button
if (isCountry) {
- html += '
';
+ html += '
';
}
- html += '
';
+ html += '
';
html += '';
$loadingChip.replaceWith(html);
@@ -4570,7 +6197,7 @@
self.serializeAllBlocks();
}
- self.updateBlockStatus($picker.closest('.target-block'));
+ self.updateBlockStatus($picker.closest('.es-block'));
});
});
@@ -4592,30 +6219,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;
}
@@ -4645,7 +6265,7 @@
$chip.append($('
';
$chipsContainer.append(html);
},
@@ -4907,12 +6527,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);
@@ -4978,7 +6597,7 @@
var method = $methodSelect.val();
if (!method) {
- $countEl.hide();
+ self._setBadgeCount($countEl, 0);
return;
}
@@ -5002,16 +6621,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', {
@@ -5036,20 +6654,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);
}
});
},
@@ -5058,6 +6669,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');
@@ -5072,12 +6684,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('
');
+ $countEl.removeClass('clickable no-matches').addClass('loading-count').show();
$.ajax({
url: this.config.ajaxUrl,
@@ -5095,19 +6707,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);
}
});
},
@@ -5187,7 +6793,7 @@
var method = $methodSelect.val();
if (!method) {
- $countEl.hide();
+ self._setBadgeCount($countEl, 0);
return;
}
@@ -5209,17 +6815,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', {
@@ -5244,21 +6849,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);
}
});
},
@@ -5290,7 +6887,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');
@@ -5306,7 +6903,7 @@
}
// Show loading
- $badge.html(this.esIcon('progress_activity', 'es-spin')).show();
+ $badge.html('
').show();
$.ajax({
url: this.config.ajaxUrl,
@@ -5324,13 +6921,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 = '
' + finalCount;
if (excludeCount > 0) {
badgeHtml += '
(-' + excludeCount + ')';
}
- $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);
@@ -5348,12 +6947,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', '–');
}
});
@@ -5364,7 +6963,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));
});
},
@@ -5419,2750 +7018,6 @@
})(jQuery);
-/**
- * Entity Selector - Groups Module
- * Selection group management, serialization, block/tab management
- * @partial _groups.js
- *
- * Contains:
- * - Group management: addGroup, removeGroup, clearAllConditions
- * - Block/Tab: switchToBlock, updateTabBadges, updateBlockStatus
- * - Serialization: serializeGroup, serializeAllBlocks, getBlockGroups
- * - Counts: fetchProductCount, updateHeaderTotalCount, updateAllConditionCounts
- * - Excludes: addFirstExcludeRow, addExcludeRow, removeExcludeRow
- * - Validation: validate, showValidationError, clearValidationError
- */
-
-(function($) {
- 'use strict';
-
- window._EntitySelectorMixins = window._EntitySelectorMixins || {};
-
- window._EntitySelectorMixins.groups = {
-
- addGroup: function($block, blockType) {
- var $container = $block.find('.groups-container');
- var trans = this.config.trans || {};
- var blockDef = this.config.blocks[blockType] || {};
- var methods = blockDef.selection_methods || {};
-
- // Remove empty state
- $container.find('.groups-empty-state').remove();
-
- // Get next group index
- var maxIndex = -1;
- $container.find('.selection-group').each(function() {
- var idx = parseInt($(this).data('groupIndex'), 10);
- if (idx > maxIndex) maxIndex = idx;
- });
- var groupIndex = maxIndex + 1;
-
- // Build method options with optgroups
- var methodOptions = this.buildMethodOptions(methods, false);
-
- // Build exclude method options (no "all") with optgroups
- var excludeMethodOptions = this.buildMethodOptions(methods, true);
-
- var defaultGroupName = (trans.group || 'Group') + ' ' + (groupIndex + 1);
- var html = '
';
-
- // Group header
- html += '';
-
- // Group body (collapsible content)
- html += '
';
-
- // Include section
- html += '
';
- html += '
';
- html += '
';
- html += '';
- html += '' + this.esIcon('visibility') + ' 0';
- html += '';
- html += '
';
- var noItemsText = trans.no_items_selected || 'No items selected - use search below';
- html += '
';
- html += '
';
- html += '
';
- html += this.esIcon('search', 'entity-search-icon');
- html += '';
- html += '' + this.esIcon('progress_activity', 'es-spin') + '';
- html += '
';
- html += '
';
- html += '
';
- html += '
';
- html += '
';
-
- // Excludes section (collapsed by default)
- html += '
';
- html += '';
- html += this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions');
- html += '';
- html += '
';
-
- // Group-level modifiers (limit & sort)
- html += '
';
- html += '';
- html += '' + (trans.limit || 'Limit') + '';
- html += '';
- html += '';
- html += '';
- html += '' + (trans.sort || 'Sort') + '';
- html += '';
- html += '';
- html += this.esIcon('sort');
- html += '';
- html += '';
- html += '';
- html += this.esIcon('visibility') + ' ';
- html += '';
- html += '
';
-
- html += '
'; // Close group-body
-
- html += '
'; // Close selection-group
-
- $container.append(html);
-
- // Find the new group and set method to "all" by default
- var $newGroup = $container.find('.selection-group[data-group-index="' + groupIndex + '"]');
-
- // Enhance the method select with styled dropdown
- this.enhanceMethodSelect($newGroup.find('.include-method-select'));
-
- $newGroup.find('.include-method-select').val('all').trigger('change');
-
- this.updateBlockStatus($block);
- this.serializeAllBlocks();
- },
-
- removeGroup: function($group, $block) {
- $group.remove();
-
- var $container = $block.find('.groups-container');
- var remainingGroups = $container.find('.selection-group').length;
-
- if (remainingGroups === 0) {
- var emptyText = this.getEmptyStateText($block);
- var emptyHtml = '
';
- emptyHtml += '' + emptyText + '';
- emptyHtml += '
';
- $container.html(emptyHtml);
- }
-
- this.updateBlockStatus($block);
- this.serializeAllBlocks();
-
- // Update tab badges and header total count
- this.updateTabBadges();
- },
-
- clearAllConditions: function() {
- var self = this;
-
- // Remove all groups from all blocks
- this.$wrapper.find('.target-block').each(function() {
- var $block = $(this);
- var $container = $block.find('.groups-container');
-
- // Remove all groups
- $container.find('.selection-group').remove();
-
- // Show empty state
- var emptyText = self.getEmptyStateText($block);
- var emptyHtml = '
';
- emptyHtml += '' + emptyText + '';
- emptyHtml += '
';
- $container.html(emptyHtml);
-
- self.updateBlockStatus($block);
- });
-
- // Update serialized data
- this.serializeAllBlocks();
-
- // Update tab badges and header count
- this.updateTabBadges();
-
- // Also update header total count immediately (since all cleared)
- this.updateHeaderTotalCount();
- },
-
- 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');
-
- // Update blocks
- this.$wrapper.find('.target-block').removeClass('active').hide();
- this.$wrapper.find('.target-block[data-block-type="' + blockType + '"]').addClass('active').show();
-
- // Close dropdown if open
- this.hideDropdown();
- },
-
- 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() {
- var $tab = $(this);
- var blockType = $tab.data('blockType');
- var $block = self.$wrapper.find('.target-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('
' + self.esIcon('progress_activity', 'es-spin') + '');
- }
- $tab.addClass('has-data');
- blockTypesWithData.push(blockType);
- } else if ($block.hasClass('custom-block')) {
- // Custom blocks: check if any input/textarea/select has a non-empty value
- var hasCustomValue = false;
- $block.find('.custom-block-content').find('input, textarea, select').each(function() {
- if ($(this).val() && $(this).val().trim() !== '') {
- hasCustomValue = true;
- return false;
- }
- });
- if (hasCustomValue) {
- if ($badge.length) {
- $badge.removeClass('loading').html(self.esIcon('check'));
- } else {
- $tab.append('
' + self.esIcon('check') + '');
- }
- $tab.addClass('has-data');
- } else {
- $badge.remove();
- $tab.removeClass('has-data');
- }
- } else {
- $badge.remove();
- $tab.removeClass('has-data');
- }
- });
-
- // 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);
- }
- },
-
- updateTargetSwitchState: function() {
- var $switch = this.$wrapper.find('.prestashop-switch');
- if (!$switch.length) {
- return;
- }
-
- // Check if any block has data
- var hasData = false;
- this.$wrapper.find('.target-block').each(function() {
- if ($(this).find('.selection-group').length > 0) {
- hasData = true;
- return false; // break
- }
- });
-
- // Update switch: value="1" is "Everyone/All/None", value="0" is "Specific/Selected"
- if (hasData) {
- $switch.find('input[value="0"]').prop('checked', true);
- } else {
- $switch.find('input[value="1"]').prop('checked', true);
- }
- },
-
- /**
- * Fetch counts for all block types in a single bulk AJAX request
- * @param {Array} blockTypes - Array of block type strings to fetch counts for
- */
- 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 {
- savedData = JSON.parse($hiddenInput.val() || '{}');
- } catch (e) {
- savedData = {};
- }
-
- // Build conditions object for all requested block types
- var conditions = {};
- blockTypes.forEach(function(blockType) {
- var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
- if (groups.length > 0) {
- conditions[blockType] = { groups: groups };
- }
- });
-
- // If no valid conditions, remove loading spinners
- 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',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'previewEntitySelectorBulk',
- trait: 'EntitySelector',
- conditions: JSON.stringify(conditions)
- },
- 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 $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 });
- }
- });
-
- // Handle any block types not in response (set count to 0 or remove badge)
- blockTypes.forEach(function(blockType) {
- if (!(blockType in response.counts)) {
- var $tab = self.$wrapper.find('.target-block-tab[data-block-type="' + blockType + '"]');
- $tab.find('.tab-badge').remove();
- $tab.removeClass('has-data');
- }
- });
-
- self.updateHeaderTotalCount();
- } else {
- 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 + '"]');
- $tab.find('.tab-badge').remove();
- });
- }
- },
- error: function(xhr, status, error) {
- 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 + '"]');
- $tab.find('.tab-badge').remove();
- });
- }
- });
- },
-
- /**
- * Fetch count for a single block type (legacy, used for single updates)
- */
- fetchProductCount: function(blockType, $tab) {
- var self = this;
- var data = {};
-
- // Read from hidden input (contains full saved data or freshly serialized data)
- var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
- var savedData = {};
- try {
- savedData = JSON.parse($hiddenInput.val() || '{}');
- } catch (e) {
- savedData = {};
- }
-
- // Get groups for the requested block type
- var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
-
- if (groups.length === 0) {
- $tab.find('.tab-badge').remove();
- $tab.removeClass('has-data');
- $tab.removeData('previewData');
- return;
- }
-
- // Show loading state
- var $badge = $tab.find('.tab-badge');
- if (!$badge.length) {
- $badge = $('
' + this.esIcon('progress_activity', 'es-spin') + '');
- $tab.append($badge);
- } else {
- $badge.addClass('loading').html(this.esIcon('progress_activity', 'es-spin'));
- }
- $tab.addClass('has-data');
-
- data[blockType] = { groups: groups };
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'previewEntitySelector',
- trait: 'EntitySelector',
- conditions: JSON.stringify(data),
- block_type: blockType,
- limit: 10
- },
- success: function(response) {
- if (response.success) {
- var $badge = $tab.find('.tab-badge');
- $badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + response.count);
-
- // Store preview data for popover
- $tab.data('previewData', response);
-
- // Update header total count
- self.updateHeaderTotalCount();
- } else {
- console.error('[EntitySelector] Preview failed for', blockType, ':', response.error || 'Unknown error');
- $tab.find('.tab-badge').remove();
- }
- },
- error: function(xhr, status, error) {
- console.error('[EntitySelector] AJAX error for', blockType, ':', status, error);
- $tab.find('.tab-badge').remove();
- self.updateHeaderTotalCount();
- }
- });
- },
-
- updateHeaderTotalCount: function() {
- var self = this;
- var total = 0;
-
- // Sum up all tab badge counts
- this.$wrapper.find('.target-block-tab .tab-badge').each(function() {
- var $badge = $(this);
- if (!$badge.hasClass('loading')) {
- var count = parseInt($badge.text(), 10);
- if (!isNaN(count)) {
- total += count;
- }
- }
- });
-
- var $totalBadge = this.$wrapper.find('.trait-total-count');
- if (total > 0) {
- $totalBadge.find('.count-value').text(total);
- $totalBadge.show();
- } else {
- $totalBadge.hide();
- }
-
- // Update show-all toggle state
- this.updateShowAllToggle();
- },
-
- updateShowAllToggle: function() {
- var $toggle = this.$wrapper.find('.trait-show-all-toggle');
- if (!$toggle.length) return;
-
- var $checkbox = $toggle.find('.show-all-checkbox');
- var hasData = this.$wrapper.find('.target-block-tab.has-data').length > 0;
-
- // If there's data, uncheck (not showing to all), otherwise check
- $checkbox.prop('checked', !hasData);
- },
-
- updateBlockStatus: function($block) {
- var $status = $block.find('.block-status');
- var blockType = $block.data('blockType');
- var blockDef = this.config.blocks[blockType] || {};
- var trans = this.config.trans || {};
-
- var groups = this.getBlockGroups($block);
-
- if (groups.length === 0) {
- var emptyMeansAll = this.config.emptyMeansAll !== false;
- if (emptyMeansAll) {
- $status.text((trans.all || 'All') + ' ' + (blockDef.entity_label_plural || 'items'));
- } else {
- $status.text(trans.nothing_selected || 'Nothing selected');
- }
- } else {
- $status.text(groups.length + ' ' + (groups.length === 1 ? (trans.group || 'group') : (trans.groups || 'groups')));
- }
- },
-
- getEmptyStateText: function($block) {
- var blockType = $block.data('blockType');
- var blockMode = $block.data('mode') || 'multi';
- var blockDef = this.config.blocks[blockType] || {};
- var trans = this.config.trans || {};
- var emptyMeansAll = this.config.emptyMeansAll !== false;
-
- if (blockMode === 'single') {
- return trans.no_item_selected || 'No item selected';
- }
-
- if (emptyMeansAll) {
- return (trans.all || 'All') + ' ' + (blockDef.entity_label_plural || 'items') + ' ' + (trans.included || 'included');
- }
-
- return trans.nothing_selected || 'Nothing selected';
- },
-
- serializeGroup: function($group, blockType) {
- var self = this;
-
- // Include
- var includeMethod = $group.find('.include-method-select').val() || 'all';
- var $includePicker = $group.find('.include-picker');
- var includeValues = this.getPickerValues($includePicker);
-
- // Excludes (multiple rows)
- var excludes = [];
- var $excludesSection = $group.find('.group-excludes.has-excludes');
- if ($excludesSection.length) {
- $group.find('.exclude-row').each(function() {
- var $row = $(this);
- var excludeMethod = $row.find('.exclude-method-select').val() || null;
- var $excludePicker = $row.find('.exclude-picker');
- var excludeValues = self.getPickerValues($excludePicker);
-
- if (excludeMethod && excludeValues && (Array.isArray(excludeValues) ? excludeValues.length > 0 : true)) {
- excludes.push({
- method: excludeMethod,
- values: excludeValues
- });
- }
- });
- }
-
- var groupData = {
- include: {
- method: includeMethod,
- values: includeValues
- }
- };
-
- if (excludes.length > 0) {
- groupData.excludes = excludes;
- }
-
- // Add modifiers if present
- var modifiers = this.getGroupModifiers($group);
- if (modifiers.limit || modifiers.sort_by) {
- groupData.modifiers = modifiers;
- }
-
- return groupData;
- },
-
- serializeAllBlocks: function($changedRow) {
- var self = this;
- var data = {};
-
- console.log('[EntitySelector] serializeAllBlocks called');
-
- this.$wrapper.find('.target-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 };
- }
-
- 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));
-
- $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);
- }
- this.countUpdateTimeout = setTimeout(function() {
- 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);
- },
-
- getBlockGroups: function($block) {
- var self = this;
- var groups = [];
-
- $block.find('.selection-group').each(function() {
- var $group = $(this);
-
- // Include
- var includeMethod = $group.find('.include-method-select').val() || 'all';
- var $includePicker = $group.find('.include-picker');
- var includeValues = self.getPickerValues($includePicker);
-
- // Skip groups with invalid include conditions (e.g., "specific products" with none selected)
- if (!self.isConditionValid(includeMethod, includeValues, $includePicker)) {
- return true; // continue to next group
- }
-
- // Excludes (multiple rows) - only include valid ones
- var excludes = [];
- var $excludesSection = $group.find('.group-excludes.has-excludes');
- if ($excludesSection.length) {
- $group.find('.exclude-row').each(function() {
- var $row = $(this);
- var excludeMethod = $row.find('.exclude-method-select').val() || null;
- var $excludePicker = $row.find('.exclude-picker');
- var excludeValues = self.getPickerValues($excludePicker);
-
- // Only include valid exclude conditions
- if (excludeMethod && self.isConditionValid(excludeMethod, excludeValues, $excludePicker)) {
- excludes.push({
- method: excludeMethod,
- values: excludeValues
- });
- }
- });
- }
-
- var groupData = {
- include: {
- method: includeMethod,
- values: includeValues
- }
- };
-
- // Group name (optional, for organizational purposes)
- var groupName = $.trim($group.attr('data-group-name') || '');
- if (groupName) {
- groupData.name = groupName;
- }
-
- if (excludes.length > 0) {
- groupData.excludes = excludes;
- }
-
- // Group-level modifiers
- var modifiers = self.getGroupModifiers($group);
- if (modifiers.limit || modifiers.sort_by) {
- groupData.modifiers = modifiers;
- }
-
- groups.push(groupData);
- });
-
- return groups;
- },
-
- getGroupModifiers: function($group) {
- var limit = $group.find('.group-modifier-limit').val();
- var sortBy = $group.find('.group-modifier-sort').val() || 'sales';
- var $sortDirBtn = $group.find('.group-modifiers .btn-sort-dir');
- var sortDir = $sortDirBtn.data('dir') || 'DESC';
-
- return {
- limit: limit ? parseInt(limit, 10) : null,
- sort_by: sortBy || null,
- sort_dir: sortDir || 'DESC'
- };
- },
-
- getPickerValues: function($picker) {
- var valueType = $picker.attr('data-value-type') || 'entity_search';
- var values = [];
-
- switch (valueType) {
- case 'entity_search':
- $picker.find('.entity-chip').each(function() {
- var id = $(this).data('id');
- values.push(isNaN(id) ? id : Number(id));
- });
- break;
-
- case 'pattern':
- values = this.getPatternTags($picker);
- // Also include draft pattern if it has content (not yet added as tag)
- var $draftInput = $picker.find('.draft-tag .pattern-input');
- var draftPattern = $.trim($draftInput.val());
- if (draftPattern) {
- var draftCaseSensitive = $draftInput.closest('.draft-tag').attr('data-case-sensitive') === '1';
- values.push({
- pattern: draftPattern,
- caseSensitive: draftCaseSensitive
- });
- }
- break;
-
- case 'numeric_range':
- var min = $picker.find('.range-min-input').val();
- var max = $picker.find('.range-max-input').val();
- if (min !== '' || max !== '') {
- values = {
- min: min !== '' ? parseFloat(min) : null,
- max: max !== '' ? parseFloat(max) : null
- };
- }
- break;
-
- case 'date_range':
- var from = $picker.find('.date-from-input').val();
- var to = $picker.find('.date-to-input').val();
- if (from || to) {
- values = {
- from: from || null,
- to: to || null
- };
- }
- break;
-
- case 'select':
- var selectVal = $picker.find('.select-value-input').val();
- if (selectVal) {
- values = [selectVal];
- }
- break;
-
- case 'boolean':
- values = [true];
- break;
-
- case 'multi_numeric_range':
- var ranges = [];
- $picker.find('.range-chip').each(function() {
- var $chip = $(this);
- var minVal = $chip.data('min');
- var maxVal = $chip.data('max');
- ranges.push({
- min: minVal !== '' && minVal !== undefined ? parseFloat(minVal) : null,
- max: maxVal !== '' && maxVal !== undefined ? parseFloat(maxVal) : null
- });
- });
- if (ranges.length > 0) {
- values = ranges;
- }
- break;
-
- case 'multi_select_tiles':
- $picker.find('.tile-option.selected').each(function() {
- values.push($(this).data('value'));
- });
- break;
-
- case 'combination_attributes':
- // Returns object: { mode: 'products'|'combinations', attributes: { groupId: [valueId1, valueId2], ... } }
- var combAttrs = {};
- $picker.find('.comb-attr-value.selected').each(function() {
- var groupId = $(this).data('groupId').toString();
- var valueId = $(this).data('valueId');
- if (!combAttrs[groupId]) {
- combAttrs[groupId] = [];
- }
- combAttrs[groupId].push(valueId);
- });
- if (Object.keys(combAttrs).length > 0) {
- // Get mode: from radio if toggle exists, otherwise from config
- var $combPicker = $picker.find('.combination-attributes-picker');
- var configMode = $combPicker.data('combinationMode') || this.config.combinationMode || 'products';
- var combMode;
- if (configMode === 'toggle') {
- combMode = $picker.find('.comb-mode-radio:checked').val() || 'products';
- } else {
- combMode = configMode;
- }
- values = {
- mode: combMode,
- attributes: combAttrs
- };
- }
- break;
- }
-
- return values;
- },
-
- isConditionValid: function(method, values, $picker) {
- // 'all' method never needs values
- if (method === 'all') {
- return true;
- }
-
- // Boolean methods are always valid (the value is implicit true)
- var valueType = $picker.attr('data-value-type') || 'entity_search';
- if (valueType === 'boolean') {
- return true;
- }
-
- // For other methods, check if values are meaningful
- if (Array.isArray(values)) {
- return values.length > 0;
- }
-
- // For object values (ranges, combination_attributes), check if meaningful
- if (typeof values === 'object' && values !== null) {
- // Special handling for combination_attributes: { mode, attributes }
- if (valueType === 'combination_attributes' && values.attributes !== undefined) {
- return Object.keys(values.attributes).length > 0;
- }
- // For ranges and other objects, check if at least one bound is set
- return Object.keys(values).some(function(key) {
- return values[key] !== null && values[key] !== '';
- });
- }
-
- return false;
- },
-
- /**
- * Update all condition counts using a single bulk AJAX request
- */
- updateAllConditionCounts: function() {
- var self = this;
- var conditions = {};
- var conditionElements = {};
- var conditionIndex = 0;
-
- // Collect all conditions from all active groups
- this.$wrapper.find('.target-block.active .selection-group').each(function() {
- var $group = $(this);
- var $block = $group.closest('.target-block');
- var blockType = $block.data('blockType') || 'products';
-
- // Process include row
- var $include = $group.find('.group-include');
- if ($include.length) {
- var includeData = self.getConditionData($include, blockType);
- if (includeData) {
- var id = 'c' + conditionIndex++;
- conditions[id] = includeData.condition;
- conditionElements[id] = includeData.$countEl;
- }
- }
-
- // Process exclude rows
- $group.find('.exclude-row').each(function() {
- var excludeData = self.getConditionData($(this), blockType);
- if (excludeData) {
- var id = 'c' + conditionIndex++;
- conditions[id] = excludeData.condition;
- conditionElements[id] = excludeData.$countEl;
- }
- });
- });
-
- // If no conditions, nothing to do
- if (Object.keys(conditions).length === 0) {
- return;
- }
-
- // Make single bulk AJAX request
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'countConditionMatchesBulk',
- trait: 'EntitySelector',
- conditions: JSON.stringify(conditions)
- },
- success: function(response) {
- if (response && response.success && response.counts) {
- // Update each count element with its result
- Object.keys(response.counts).forEach(function(id) {
- var count = response.counts[id] || 0;
- var $countEl = conditionElements[id];
- if ($countEl && $countEl.length) {
- $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();
- }
- }
- });
- }
- // Note: Group totals are updated on-demand when user interacts, not on initial load
- },
- error: function() {
- // Hide all count elements on error
- Object.keys(conditionElements).forEach(function(id) {
- var $countEl = conditionElements[id];
- if ($countEl && $countEl.length) {
- $countEl.hide().removeClass('clickable');
- }
- });
- }
- });
- },
-
- /**
- * 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');
- var $methodSelect = isExclude
- ? $row.find('.exclude-method-select')
- : $row.find('.include-method-select');
-
- var method = $methodSelect.val();
- console.log('[getConditionData] method:', method);
- if (!method) {
- $countEl.hide();
- 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() {
- self.updateConditionCount($row, blockType);
- }, 0);
- return null; // Skip bulk processing, handled separately
- }
-
- // 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);
- }, 0);
- return null; // Skip bulk processing, handled separately
- }
-
- // Hide badge for other "all" type methods (valueType === 'none') since they don't filter
- if (valueType === 'none') {
- $countEl.hide();
- return null;
- }
-
- var values = this.getPickerValues($picker, valueType);
-
- // Don't count if no values (except for boolean methods)
- var hasNoValues = !values ||
- (Array.isArray(values) && values.length === 0) ||
- (typeof values === 'object' && !Array.isArray(values) && (
- (valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
- (valueType !== 'combination_attributes' && Object.keys(values).length === 0)
- ));
- if (valueType !== 'boolean' && hasNoValues) {
- $countEl.hide();
- return null;
- }
-
- // Show loading spinner
- $countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
- $countEl.removeClass('clickable no-matches').show();
-
- // Store condition data on badge for popover
- $countEl.data('conditionData', {
- method: method,
- values: values,
- blockType: blockType,
- isExclude: isExclude
- });
-
- return {
- condition: {
- method: method,
- values: values,
- block_type: blockType
- },
- $countEl: $countEl
- };
- },
-
- updateGroupCounts: function($group) {
- var self = this;
- var $block = $group.closest('.target-block');
- var blockType = $block.data('blockType') || 'products';
-
- // Update include count
- var $include = $group.find('.group-include');
- if ($include.length) {
- this.updateConditionCount($include, blockType);
- }
-
- // Update each exclude row count
- $group.find('.exclude-row').each(function() {
- self.updateConditionCount($(this), blockType);
- });
-
- // Update group total count (include - excludes)
- this.updateGroupTotalCount($group);
- },
-
- /**
- * Update a single condition count (used for individual updates after user changes)
- */
- updateConditionCount: function($row, blockType) {
- var self = this;
-
- 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;
- }
-
- var isExclude = $row.hasClass('exclude-row');
- var $methodSelect = isExclude
- ? $row.find('.exclude-method-select')
- : $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();
- return;
- }
-
- var $picker = isExclude
- ? $row.find('.exclude-picker')
- : $row.find('.include-picker');
-
- var valueType = $picker.data('valueType') || 'none';
- var searchEntity = $picker.attr('data-search-entity') || '';
-
- // Get the block type to check if this is a countries block
- if (!blockType) {
- var $block = $row.closest('.target-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();
-
- // First fetch all active country IDs, then get holidays
- $.ajax({
- url: self.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'searchTargetEntities',
- trait: 'EntitySelector',
- entity_type: 'countries',
- query: '',
- limit: 500
- },
- success: function(response) {
- 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', {
- method: method,
- values: allCountryIds,
- blockType: blockType,
- isExclude: isExclude,
- isCountryHolidays: true,
- countryIds: allCountryIds,
- isAllCountries: true
- });
-
- // Now fetch holiday count
- $.ajax({
- url: self.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'getHolidaysForCountries',
- trait: 'EntitySelector',
- country_ids: allCountryIds.join(','),
- 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();
- } else {
- $countEl.find('.preview-count').text(count);
- $countEl.addClass('clickable').show();
- }
- $countEl.data('countriesInfo', holidayResponse.countries || []);
- } else {
- $countEl.hide().removeClass('clickable');
- }
- },
- error: function() {
- $countEl.hide().removeClass('clickable');
- }
- });
- } else {
- $countEl.hide().removeClass('clickable');
- }
- },
- error: function() {
- $countEl.hide().removeClass('clickable');
- }
- });
- return;
- }
-
- // Hide badge for other "all" type methods (valueType === 'none') since they don't filter
- if (valueType === 'none') {
- console.log('[updateConditionCount] valueType is none, hiding badge');
- $countEl.hide();
- return;
- }
-
- var values = this.getPickerValues($picker, valueType);
-
- var hasNoValues = !values ||
- (Array.isArray(values) && values.length === 0) ||
- (typeof values === 'object' && !Array.isArray(values) && (
- (valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
- (valueType !== 'combination_attributes' && Object.keys(values).length === 0)
- ));
- if (valueType !== 'boolean' && hasNoValues) {
- $countEl.hide();
- return;
- }
-
- if (!blockType) {
- var $block = $row.closest('.target-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();
-
- // 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,
- blockType: blockType,
- isExclude: isExclude,
- isCountryHolidays: true,
- countryIds: values
- });
-
- $.ajax({
- url: self.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'getHolidaysForCountries',
- trait: 'EntitySelector',
- country_ids: values.join(','),
- 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();
- } else {
- $countEl.find('.preview-count').text(count);
- $countEl.addClass('clickable').show();
- }
- // Store countries info for popover
- $countEl.data('countriesInfo', response.countries || []);
- } else {
- console.log('[updateConditionCount] Holiday response failed:', response);
- $countEl.hide().removeClass('clickable');
- }
- },
- error: function() {
- $countEl.hide().removeClass('clickable');
- }
- });
- return;
- }
-
- // Default: count entities
- $countEl.data('conditionData', {
- method: method,
- values: values,
- blockType: blockType,
- isExclude: isExclude
- });
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'countConditionMatches',
- trait: 'EntitySelector',
- method: method,
- values: JSON.stringify(values),
- block_type: blockType
- },
- 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();
- }
- } else {
- $countEl.hide().removeClass('clickable');
- }
- },
- error: function() {
- $countEl.hide().removeClass('clickable');
- }
- });
- },
-
- updateGroupTotalCount: function($group) {
- var self = this;
- var $block = $group.closest('.target-block');
- var blockType = $block.data('blockType') || 'products';
- var $badge = $group.find('.group-header .group-count-badge');
- var $limitInput = $group.find('.group-modifier-limit');
-
- // Build group data for AJAX
- var groupData = this.serializeGroup($group, blockType);
-
- // Check if include has valid data
- if (!groupData.include || !groupData.include.method) {
- $badge.hide();
- $limitInput.attr('placeholder', '–');
- return;
- }
-
- // Show loading
- $badge.html(this.esIcon('progress_activity', 'es-spin')).show();
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'countGroupItems',
- trait: 'EntitySelector',
- group_data: JSON.stringify(groupData),
- block_type: blockType
- },
- success: function(response) {
- if (response && response.success) {
- 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;
- if (excludeCount > 0) {
- badgeHtml += '
(-' + excludeCount + ')';
- }
- $badge.html(badgeHtml);
- $badge.addClass('clickable').show();
-
- // Store group data on badge for preview popover
- $badge.data('groupData', groupData);
- $badge.data('blockType', blockType);
- $badge.data('finalCount', finalCount);
-
- // Update limit placeholder with the count
- $limitInput.attr('placeholder', finalCount);
-
- // Also update the group-preview-badge count (apply limit if set)
- var $previewBadge = $group.find('.group-preview-badge .preview-count');
- if ($previewBadge.length) {
- var limit = parseInt($limitInput.val(), 10);
- var displayCount = (limit > 0 && limit < finalCount) ? limit : finalCount;
- $previewBadge.text(displayCount);
- }
- } else {
- $badge.hide().removeClass('clickable');
- $limitInput.attr('placeholder', '–');
- }
- },
- error: function() {
- $badge.hide();
- $limitInput.attr('placeholder', '–');
- }
- });
- },
-
- // Exclude row management
- addFirstExcludeRow: function($group, $block) {
- var $excludesDiv = $group.find('.group-excludes');
- var trans = this.config.trans || {};
-
- // Build the full excludes structure with first row
- var html = '
';
- html += '' + this.esIcon('block') + ' ' + (trans.except || 'EXCEPT') + '';
- html += '
';
-
- html += '
';
- html += this.buildExcludeRowHtml($block, 0);
- html += '
';
-
- html += '
';
- html += this.esIcon('add') + ' ' + (trans.add_another_exception || 'Add another exception');
- html += '';
-
- $excludesDiv.addClass('has-excludes').html(html);
-
- // Enhance the first exclude method select with styled dropdown
- var $firstRow = $excludesDiv.find('.exclude-row[data-exclude-index="0"]');
- var $firstSelect = $firstRow.find('.exclude-method-select');
- this.enhanceMethodSelect($firstSelect);
-
- // Update method info placeholder for initial selection
- var blockType = $block.data('blockType');
- var initialMethod = $firstSelect.val();
- this.updateMethodInfoPlaceholder($firstRow.find('.method-selector-wrapper'), initialMethod, blockType);
-
- this.updateMethodSelectorLock($group, true);
- this.serializeAllBlocks();
- },
-
- addExcludeRow: function($group, $block) {
- var $container = $group.find('.exclude-rows-container');
-
- // Get next exclude index
- var maxIndex = -1;
- $container.find('.exclude-row').each(function() {
- var idx = parseInt($(this).data('excludeIndex'), 10);
- if (idx > maxIndex) maxIndex = idx;
- });
- var excludeIndex = maxIndex + 1;
-
- var html = this.buildExcludeRowHtml($block, excludeIndex);
- $container.append(html);
-
- // Enhance the exclude method select with styled dropdown
- var $newRow = $container.find('.exclude-row[data-exclude-index="' + excludeIndex + '"]');
- var $newSelect = $newRow.find('.exclude-method-select');
- this.enhanceMethodSelect($newSelect);
-
- // Update method info placeholder for initial selection
- var blockType = $block.data('blockType');
- var initialMethod = $newSelect.val();
- this.updateMethodInfoPlaceholder($newRow.find('.method-selector-wrapper'), initialMethod, blockType);
-
- this.serializeAllBlocks();
- },
-
- buildExcludeRowHtml: function($block, excludeIndex) {
- var blockType = $block.data('blockType');
- var blockDef = this.config.blocks[blockType] || {};
- var methods = blockDef.selection_methods || {};
- var trans = this.config.trans || {};
-
- // Build exclude method options with optgroups (no "all")
- var excludeMethodOptions = this.buildMethodOptions(methods, true);
-
- // Find first non-all method for default search entity
- var firstSearchEntity = blockType;
- var firstValueType = 'entity_search';
- $.each(methods, function(methodKey, methodDef) {
- if (methodKey === 'all') return true;
- firstSearchEntity = methodDef.search_entity || blockType;
- firstValueType = methodDef.value_type || 'entity_search';
- return false; // break
- });
-
- var html = '
';
-
- // Header row with method select wrapped in method-selector-wrapper (same as include)
- html += '';
-
- // Value picker based on first method's value type
- html += this.buildValuePickerHtml('exclude', firstValueType, firstSearchEntity, methods);
-
- html += '
';
-
- return html;
- },
-
- removeExcludeRow: function($excludeRow, $group, $block) {
- var $container = $group.find('.exclude-rows-container');
- var trans = this.config.trans || {};
-
- $excludeRow.remove();
-
- // Check if there are remaining exclude rows
- var remainingRows = $container.find('.exclude-row').length;
-
- if (remainingRows === 0) {
- // Remove entire excludes section and show "Add exceptions" button
- var $excludesDiv = $group.find('.group-excludes');
- $excludesDiv.removeClass('has-excludes').html(
- '
' +
- this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions') +
- ''
- );
- // Unlock the method selector since no excludes exist
- this.updateMethodSelectorLock($group, false);
- }
-
- this.serializeAllBlocks();
- },
-
- // Method options building
- buildMethodOptions: function(methods, excludeAll) {
- var self = this;
- var trans = this.config.trans || {};
- var html = '';
-
- // Group labels
- var groupLabels = {
- 'select_by': trans.select_by || 'Select by...',
- 'filter_by': trans.filter_by || 'Filter by...'
- };
-
- // Separate methods by group
- var grouped = {};
- var ungrouped = {};
-
- $.each(methods, function(methodKey, methodDef) {
- if (excludeAll && methodKey === 'all') return true; // skip
-
- var group = methodDef.group || '';
- if (group) {
- if (!grouped[group]) {
- grouped[group] = {};
- }
- grouped[group][methodKey] = methodDef;
- } else {
- ungrouped[methodKey] = methodDef;
- }
- });
-
- // Render ungrouped options first
- $.each(ungrouped, function(methodKey, methodDef) {
- html += self.buildMethodOption(methodKey, methodDef);
- });
-
- // Render grouped options with optgroups
- $.each(grouped, function(groupKey, groupMethods) {
- var groupLabel = groupLabels[groupKey] || groupKey.replace(/_/g, ' ');
- html += '
';
- });
-
- return html;
- },
-
- buildMethodOption: function(methodKey, methodDef) {
- var html = '
';
- return html;
- },
-
- buildValuePickerHtml: function(section, valueType, searchEntity, methods) {
- var trans = this.config.trans || {};
- var pickerClass = section + '-picker';
- var chipsClass = section + '-chips';
- var dataClass = section + '-values-data';
- var html = '';
-
- if (valueType === 'none') {
- html = '
';
- html += '';
- html += '
';
- return html;
- }
-
- html = '
';
-
- switch (valueType) {
- case 'entity_search':
- var noItemsText = trans.no_items_selected || 'No items selected - use search below';
- html += '
';
- html += '
';
- html += this.esIcon('search', 'entity-search-icon');
- html += '';
- html += '' + this.esIcon('progress_activity', 'es-spin') + '';
- html += '
';
- html += '
';
- break;
-
- case 'pattern':
- // Build tooltip content for data-details attribute
- var tooltipContent = '
' + this.escapeHtml(trans.pattern_help_title || 'Pattern Syntax') + '';
- tooltipContent += '
';
- tooltipContent += '
* ' + this.escapeHtml(trans.pattern_help_wildcard || 'any text (wildcard)') + '
';
- tooltipContent += '
{number} ' + this.escapeHtml(trans.pattern_help_number || 'any number (e.g. 100, 250)') + '
';
- tooltipContent += '
{letter} ' + this.escapeHtml(trans.pattern_help_letter || 'single letter (A-Z)') + '
';
- tooltipContent += '
';
- tooltipContent += '
';
- tooltipContent += '
' + this.escapeHtml(trans.pattern_help_examples || 'Examples:') + '';
- tooltipContent += '
*cotton* ' + this.escapeHtml(trans.pattern_example_1 || 'contains "cotton"') + '
';
- tooltipContent += '
iPhone {number} Pro* ' + this.escapeHtml(trans.pattern_example_2 || 'matches "iPhone 15 Pro Max"') + '
';
- tooltipContent += '
Size {letter} ' + this.escapeHtml(trans.pattern_example_3 || 'matches "Size M", "Size L"') + '
';
- tooltipContent += '
';
-
- var noPatternText = trans.no_patterns || 'No patterns - press Enter to add';
- html += '
';
- html += '
';
- html += '
';
- break;
-
- case 'numeric_range':
- html += '
';
- html += '';
- html += '-';
- html += '';
- html += '
';
- html += '
';
- break;
-
- case 'multi_numeric_range':
- html += '
';
- html += '
';
- break;
-
- case 'multi_select_tiles':
- html += '
';
- // Tiles will be populated based on method options
- html += '
';
- html += '
';
- break;
-
- case 'date_range':
- html += '
';
- html += '';
- html += '-';
- html += '';
- html += '
';
- html += '
';
- break;
-
- case 'select':
- html += '
';
- html += '';
- html += '
';
- html += '
';
- break;
-
- case 'boolean':
- html += '
';
- html += '' + this.escapeHtml(trans.yes || 'Yes') + '';
- html += '
';
- html += '
';
- break;
-
- case 'combination_attributes':
- // Build tooltip content
- var combTooltip = '
' + this.escapeHtml(trans.combination_help_title || 'Combination Targeting') + '';
- combTooltip += '
';
- combTooltip += '
' + this.escapeHtml(trans.combination_help_desc || 'Select attributes to target specific product combinations.') + '
';
- combTooltip += '
' + this.escapeHtml(trans.combination_help_logic || 'Logic:') + '
';
- combTooltip += '
';
- combTooltip += '- ' + this.escapeHtml(trans.combination_help_within || 'Within group: OR (Red OR Blue)') + '
';
- combTooltip += '- ' + this.escapeHtml(trans.combination_help_between || 'Between groups: AND (Color AND Size)') + '
';
- combTooltip += '
';
- combTooltip += '
';
-
- // Combination mode from config: 'products', 'combinations', or 'toggle'
- var combMode = this.config.combinationMode || 'products';
- var showModeToggle = (combMode === 'toggle');
- var defaultMode = showModeToggle ? 'products' : combMode;
-
- html += '
';
- // Store mode along with attributes: { mode: 'products'|'combinations', attributes: { groupId: [valueIds] } }
- html += '
';
- break;
-
- default:
- html += '
';
- break;
- }
-
- html += '
';
- return html;
- },
-
- // Sort options
- getSortOptionsArray: function(blockType) {
- var trans = this.config.trans || {};
-
- switch (blockType) {
- case 'products':
- return [
- { value: 'sales', label: trans.sort_bestsellers || 'Best sellers' },
- { value: 'date_add', label: trans.sort_newest || 'Newest' },
- { value: 'price', label: trans.sort_price || 'Price' },
- { value: 'name', label: trans.sort_name || 'Name' },
- { value: 'position', label: trans.sort_position || 'Position' },
- { value: 'quantity', label: trans.sort_stock || 'Stock quantity' },
- { value: 'random', label: trans.sort_random || 'Random' }
- ];
- case 'categories':
- return [
- { value: 'name', label: trans.sort_name || 'Name' },
- { value: 'position', label: trans.sort_position || 'Position' },
- { value: 'product_count', label: trans.sort_products || 'Products count' },
- { value: 'date_add', label: trans.sort_newest || 'Newest' }
- ];
- default:
- return [
- { value: 'name', label: trans.sort_name || 'Name' },
- { value: 'date_add', label: trans.sort_newest || 'Newest' }
- ];
- }
- },
-
- getSortIconName: function(sortBy, sortDir) {
- switch (sortBy) {
- case 'name':
- return 'sort_by_alpha';
- case 'random':
- return 'shuffle';
- default:
- return 'sort';
- }
- },
-
- cycleSortOption: function($btn, blockType) {
- var sortOptions = this.getSortOptionsArray(blockType);
- var currentSort = $btn.data('sort') || 'sales';
- var currentDir = $btn.data('dir') || 'DESC';
-
- // Find current index
- var currentIndex = -1;
- for (var i = 0; i < sortOptions.length; i++) {
- if (sortOptions[i].value === currentSort) {
- currentIndex = i;
- break;
- }
- }
-
- // Cycle: first toggle direction, then move to next sort option
- var newSort, newDir, newLabel;
- if (currentDir === 'DESC') {
- // Toggle to ASC, same sort
- newSort = currentSort;
- newDir = 'ASC';
- } else {
- // Move to next sort option, reset to DESC
- var nextIndex = (currentIndex + 1) % sortOptions.length;
- newSort = sortOptions[nextIndex].value;
- newDir = 'DESC';
- }
-
- // Find label for new sort
- for (var j = 0; j < sortOptions.length; j++) {
- if (sortOptions[j].value === newSort) {
- newLabel = sortOptions[j].label;
- break;
- }
- }
-
- // Update button
- $btn.data('sort', newSort);
- $btn.data('dir', newDir);
- $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)));
- },
-
- // Validation
- validate: function() {
- var isRequired = this.$wrapper.data('required') === 1 || this.$wrapper.data('required') === '1';
- if (!isRequired) {
- return true;
- }
-
- // Check if any block has data (groups with selections)
- var hasData = false;
- this.$wrapper.find('.target-block').each(function() {
- if ($(this).find('.selection-group').length > 0) {
- hasData = true;
- return false; // break
- }
- });
-
- if (!hasData) {
- // Show validation error
- this.showValidationError();
- return false;
- }
-
- // Valid - remove any previous error
- this.clearValidationError();
- return true;
- },
-
- showValidationError: function() {
- this.$wrapper.addClass('has-validation-error');
- var message = this.$wrapper.data('required-message') || 'Please select at least one item';
-
- // Remove any existing error
- this.$wrapper.find('.trait-validation-error').remove();
-
- // Add error message after header
- var $error = $('
', {
- class: 'trait-validation-error',
- html: this.esIcon('warning') + ' ' + message
- });
- this.$wrapper.find('.condition-trait-header').after($error);
-
- // Scroll to error
- $('html, body').animate({
- scrollTop: this.$wrapper.offset().top - 100
- }, 300);
-
- // Expand the trait if collapsed
- if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
- this.$wrapper.find('.condition-trait-body').slideDown(200);
- this.$wrapper.removeClass('collapsed');
- }
- },
-
- clearValidationError: function() {
- this.$wrapper.removeClass('has-validation-error');
- this.$wrapper.find('.trait-validation-error').remove();
- }
- };
-
-})(jQuery);
-
-/**
- * Entity Selector - Methods Module
- * Method dropdown rendering, value pickers, combination picker
- * @partial _methods.js
- *
- * EXTRACTION SOURCE: assets/js/admin/entity-selector.js
- * Lines: 6760-6848 (initMethodDropdowns, enhanceMethodSelect)
- * 6849-7051 (showMethodDropdownMenu, buildMethodDropdownMenuHtml, closeMethodDropdownMenu)
- * 7053-7138 (populateTiles, applyRangeInputConstraints, showRangeInputError)
- * 7139-7380 (combination picker methods)
- * 7382-7550 (updateMethodInfoPlaceholder, getBuiltInMethodHelp)
- * 7748-7888 (buildSortOptions, updateModifierButtonState, updateMethodSelectorLock)
- *
- * Contains:
- * - initMethodDropdowns() - Initialize styled dropdowns
- * - enhanceMethodSelect() - Convert select to styled dropdown
- * - showMethodDropdownMenu() - Show method selection menu
- * - buildMethodDropdownMenuHtml() - Build menu HTML
- * - closeMethodDropdownMenu() - Close dropdown menu
- * - updateMethodTrigger() - Update trigger display
- * - populateTiles() - Build multi-select tiles
- * - applyRangeInputConstraints() - Set numeric input constraints
- * - showRangeInputError() - Display validation error
- * - loadCombinationAttributeGroups() - Load attribute groups for picker
- * - loadCombinationAttributeValues() - Load values for attribute group
- * - restoreCombinationSelections() - Restore saved combination state
- * - updateCombinationData() - Save combination selection
- * - updateCombinationGroupCounts() - Update selection counts
- * - updateMethodInfoPlaceholder() - Show method help
- * - getBuiltInMethodHelp() - Get help text for methods
- * - buildSortOptions() - Build sort dropdown options
- * - updateModifierButtonState() - Update modifier toggle state
- * - updateMethodSelectorLock() - Lock/unlock method selector
- */
-
-(function($) {
- 'use strict';
-
- window._EntitySelectorMixins = window._EntitySelectorMixins || {};
-
- window._EntitySelectorMixins.methods = {
-
- /**
- * Initialize styled method dropdowns for all method selects
- */
- initMethodDropdowns: function() {
- var self = this;
- this.$wrapper.find('.include-method-select').each(function() {
- self.enhanceMethodSelect($(this));
- });
- this.$wrapper.find('.exclude-method-select').each(function() {
- self.enhanceMethodSelect($(this));
- });
- this.initMethodInfoPlaceholders();
- },
-
- /**
- * Initialize info placeholders for all existing method selects
- */
- initMethodInfoPlaceholders: function() {
- var self = this;
- this.$wrapper.find('.selection-group').each(function() {
- var $group = $(this);
- var $block = $group.closest('.target-block');
- var blockType = $block.data('blockType') || 'products';
-
- // Include method info
- var includeMethod = $group.find('.include-method-select').val() || 'all';
- self.updateMethodInfoPlaceholder($group.find('.method-selector-wrapper'), includeMethod, blockType);
-
- // Exclude methods info
- $group.find('.exclude-row').each(function() {
- var $row = $(this);
- var excludeMethod = $row.find('.exclude-method-select').val();
- if (excludeMethod) {
- self.updateMethodInfoPlaceholder($row.find('.method-selector-wrapper'), excludeMethod, blockType);
- }
- });
- });
- },
-
- /**
- * Enhance a single method select with styled dropdown
- */
- enhanceMethodSelect: function($select) {
- var self = this;
-
- if (!$select.length || $select.data('methodDropdownInit')) {
- 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 selectedLabel = $selectedOption.text();
-
- var triggerHtml = '
';
- triggerHtml += this.esIcon(selectedIcon, 'method-trigger-icon');
- triggerHtml += '' + this.escapeHtml(selectedLabel) + '';
- triggerHtml += this.esIcon('arrow_drop_down', 'method-trigger-caret');
- triggerHtml += '
';
-
- var $trigger = $(triggerHtml);
- $select.after($trigger);
-
- $trigger.on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- var $wrapper = $select.closest('.method-selector-wrapper');
- if ($wrapper.hasClass('selector-locked')) {
- return;
- }
-
- self.showMethodDropdownMenu($select, $trigger);
- });
-
- $select.on('change.methodDropdown', function() {
- self.updateMethodTrigger($select, $trigger);
- });
- },
-
- /**
- * Update the trigger display to match current selection
- */
- updateMethodTrigger: function($select, $trigger) {
- var $selectedOption = $select.find('option:selected');
- var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
- var selectedLabel = $selectedOption.text();
-
- $trigger.find('.method-trigger-icon').replaceWith(this.esIcon(selectedIcon, 'method-trigger-icon'));
- $trigger.find('.method-trigger-label').text(selectedLabel);
- },
-
- /**
- * Show the method dropdown menu
- */
- showMethodDropdownMenu: function($select, $trigger) {
- var self = this;
-
- this.closeMethodDropdownMenu();
-
- var menuHtml = this.buildMethodDropdownMenuHtml($select);
- var $menu = $(menuHtml);
-
- var triggerOffset = $trigger.offset();
- var triggerWidth = $trigger.outerWidth();
- var triggerHeight = $trigger.outerHeight();
-
- $menu.css({
- position: 'absolute',
- top: triggerOffset.top + triggerHeight + 2,
- left: triggerOffset.left,
- minWidth: triggerWidth,
- zIndex: 10001
- });
-
- $('body').append($menu);
- this.$methodDropdownMenu = $menu;
- this.$methodDropdownSelect = $select;
- this.$methodDropdownTrigger = $trigger;
-
- $menu.on('click', '.method-dropdown-item', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- var value = $(this).data('value');
- $select.val(value).trigger('change');
- self.closeMethodDropdownMenu();
- });
-
- $(document).on('click.methodDropdown', function(e) {
- if (!$(e.target).closest('.method-dropdown-menu, .method-dropdown-trigger').length) {
- self.closeMethodDropdownMenu();
- }
- });
-
- $(document).on('keydown.methodDropdown', function(e) {
- if (e.keyCode === 27) {
- self.closeMethodDropdownMenu();
- }
- });
- },
-
- /**
- * Build the dropdown menu HTML
- */
- buildMethodDropdownMenuHtml: function($select) {
- var self = this;
- var html = '';
- return html;
- },
-
- /**
- * Close the method dropdown menu
- */
- closeMethodDropdownMenu: function() {
- if (this.$methodDropdownMenu) {
- this.$methodDropdownMenu.remove();
- this.$methodDropdownMenu = null;
- }
- this.$methodDropdownSelect = null;
- this.$methodDropdownTrigger = null;
- $(document).off('click.methodDropdown keydown.methodDropdown');
- },
-
- /**
- * Populate tiles for multi_select_tiles value picker
- */
- populateTiles: function($picker, options, exclusive) {
- var self = this;
- var $container = $picker.find('.multi-select-tiles');
- $container.empty();
-
- if (exclusive) {
- $container.attr('data-exclusive', 'true');
- } else {
- $container.removeAttr('data-exclusive');
- }
-
- $.each(options, function(key, optData) {
- var label = typeof optData === 'object' ? optData.label : optData;
- var icon = typeof optData === 'object' && optData.icon ? optData.icon : null;
- var color = typeof optData === 'object' && optData.color ? optData.color : null;
-
- var tileClass = 'tile-option';
- if (color) {
- tileClass += ' tile-color-' + color;
- }
-
- var $tile = $('
', {
- type: 'button',
- class: tileClass,
- 'data-value': key
- });
-
- if (icon) {
- $tile.append($('', { class: icon }));
- }
- $tile.append($('', { class: 'tile-label', text: label }));
-
- $container.append($tile);
- });
- },
-
- /**
- * Apply step/min constraints to numeric range inputs
- */
- applyRangeInputConstraints: function($picker, step, min) {
- var $inputs = $picker.find('.range-min-input, .range-max-input');
-
- if (typeof step !== 'undefined' && step !== null) {
- $inputs.attr('step', step);
- } else {
- $inputs.attr('step', 'any');
- }
-
- if (typeof min !== 'undefined' && min !== null) {
- $inputs.attr('min', min);
- } else {
- $inputs.removeAttr('min');
- }
- },
-
- /**
- * Show error message on range input
- */
- showRangeInputError: function($input, message) {
- var $container = $input.closest('.multi-range-input-row');
-
- $container.find('.range-input-error').remove();
- $container.find('.range-min-input, .range-max-input').removeClass('has-error');
-
- $input.addClass('has-error');
- var $error = $('', {
- class: 'range-input-error',
- text: message
- });
- $container.append($error);
-
- setTimeout(function() {
- $input.removeClass('has-error');
- $error.fadeOut(200, function() {
- $(this).remove();
- });
- }, 3000);
- },
-
- /**
- * Load attribute groups for combination picker
- */
- loadCombinationAttributeGroups: function($picker) {
- var self = this;
- var trans = this.config.trans || {};
- var $container = $picker.find('.combination-groups-container');
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'getAttributeGroups',
- trait: 'TargetConditions'
- },
- success: function(response) {
- $container.empty();
-
- if (!response.success || !response.groups || response.groups.length === 0) {
- $container.html('' +
- self.escapeHtml(trans.no_attribute_groups || 'No attribute groups found') +
- '');
- return;
- }
-
- response.groups.forEach(function(group) {
- var $groupDiv = $('', {
- class: 'comb-attr-group',
- 'data-group-id': group.id
- });
-
- var $groupHeader = $('
', { class: 'comb-attr-group-header' });
- $groupHeader.append($('
', {
- class: 'comb-attr-group-name',
- text: group.name
- }));
- $groupHeader.append($('', {
- class: 'comb-attr-group-count',
- text: '0'
- }));
-
- var $toolbar = $('', { class: 'comb-attr-toolbar' });
- $toolbar.append($('
', {
- type: 'button',
- class: 'comb-toolbar-btn comb-select-all',
- title: trans.select_all || 'Select all',
- html: self.esIcon('check_box')
- }));
- $toolbar.append($('', {
- type: 'button',
- class: 'comb-toolbar-btn comb-select-none',
- title: trans.clear || 'Clear',
- html: self.esIcon('check_box_outline_blank')
- }));
- $toolbar.append($('', {
- type: 'text',
- class: 'comb-attr-search',
- placeholder: trans.filter_results || 'Filter...'
- }));
-
- var $valuesContainer = $('', {
- class: 'comb-attr-values',
- 'data-loaded': 'false'
- });
- $valuesContainer.append($('
', {
- class: 'comb-attr-loading',
- html: self.esIcon('progress_activity', 'es-spin')
- }));
-
- $groupDiv.append($groupHeader);
- $groupDiv.append($toolbar);
- $groupDiv.append($valuesContainer);
- $container.append($groupDiv);
-
- self.loadCombinationAttributeValues($picker, group.id, $valuesContainer);
- });
- },
- error: function() {
- $container.html('' +
- self.escapeHtml(trans.error_loading || 'Error loading attribute groups') +
- '');
- }
- });
- },
-
- /**
- * Load attribute values for a specific group
- */
- loadCombinationAttributeValues: function($picker, groupId, $container) {
- var self = this;
- var trans = this.config.trans || {};
-
- $.ajax({
- url: this.config.ajaxUrl,
- type: 'POST',
- dataType: 'json',
- data: {
- ajax: 1,
- action: 'getAttributeValues',
- trait: 'TargetConditions',
- id_attribute_group: groupId
- },
- success: function(response) {
- $container.empty();
- $container.attr('data-loaded', 'true');
-
- if (!response.success || !response.values || response.values.length === 0) {
- $container.html('' +
- self.escapeHtml(trans.no_values || 'No values') +
- '');
- return;
- }
-
- response.values.forEach(function(value) {
- var productCount = parseInt(value.product_count) || 0;
- var $valueBtn = $('', {
- type: 'button',
- class: 'comb-attr-value',
- 'data-value-id': value.id,
- 'data-group-id': groupId,
- 'data-name': value.name.toLowerCase()
- });
- $valueBtn.append($('', {
- class: 'comb-attr-value-name',
- text: value.name
- }));
- if (productCount > 0) {
- $valueBtn.append($('', {
- class: 'comb-attr-value-count',
- text: productCount
- }));
- }
- $container.append($valueBtn);
- });
-
- self.restoreCombinationSelections($picker);
- },
- error: function() {
- $container.html('' +
- self.escapeHtml(trans.error_loading || 'Error') +
- '');
- }
- });
- },
-
- /**
- * Restore previously selected combination values from hidden input
- */
- restoreCombinationSelections: function($picker) {
- var $dataInput = $picker.find('.include-values-data, .exclude-values-data').first();
- var dataStr = $dataInput.val() || '{}';
- var data;
-
- try {
- data = JSON.parse(dataStr);
- } catch (e) {
- return;
- }
-
- var attributes = data.attributes || data;
- var mode = data.mode || 'products';
-
- $picker.find('.comb-mode-radio[value="' + mode + '"]').prop('checked', true);
-
- $.each(attributes, function(groupId, valueIds) {
- if (!Array.isArray(valueIds)) return;
-
- valueIds.forEach(function(valueId) {
- $picker.find('.comb-attr-value[data-group-id="' + groupId + '"][data-value-id="' + valueId + '"]')
- .addClass('selected');
- });
- });
-
- this.updateCombinationGroupCounts($picker);
- },
-
- /**
- * Update hidden input with current combination selections
- */
- updateCombinationData: function($picker) {
- var attributes = {};
-
- $picker.find('.comb-attr-value.selected').each(function() {
- var groupId = $(this).data('groupId').toString();
- var valueId = $(this).data('valueId');
-
- if (!attributes[groupId]) {
- attributes[groupId] = [];
- }
- attributes[groupId].push(valueId);
- });
-
- var $combPicker = $picker.find('.combination-attributes-picker');
- var configMode = $combPicker.data('combinationMode') || this.config.combinationMode || 'products';
- var mode;
-
- if (configMode === 'toggle') {
- mode = $picker.find('.comb-mode-radio:checked').val() || 'products';
- } else {
- mode = configMode;
- }
-
- var data = {
- mode: mode,
- attributes: attributes
- };
-
- var $dataInput = $picker.find('.include-values-data, .exclude-values-data').first();
- $dataInput.val(JSON.stringify(data));
-
- this.updateCombinationGroupCounts($picker);
- },
-
- /**
- * Update the count badges on each attribute group
- */
- updateCombinationGroupCounts: function($picker) {
- $picker.find('.comb-attr-group').each(function() {
- var $group = $(this);
- var count = $group.find('.comb-attr-value.selected').length;
- $group.find('.comb-attr-group-count').text(count);
-
- if (count > 0) {
- $group.addClass('has-selections');
- } else {
- $group.removeClass('has-selections');
- }
- });
- },
-
- /**
- * Update the info placeholder based on method and block type
- */
- updateMethodInfoPlaceholder: function($headerRow, method, blockType) {
- var $placeholder = $headerRow.find('.method-info-placeholder');
- if (!$placeholder.length) return;
-
- $placeholder.empty();
-
- var methodHelp = this.config.methodHelp || {};
- var blockHelp = methodHelp[blockType] || methodHelp['products'] || {};
- var helpContent = blockHelp[method] || this.getBuiltInMethodHelp(method);
-
- if (helpContent) {
- var $infoWrapper = $('', {
- 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();
- }
- }
- },
-
- /**
- * Get built-in help content for targeting methods
- */
- getBuiltInMethodHelp: function(method) {
- var trans = this.config.trans || {};
- var html = '';
-
- switch (method) {
- case 'all':
- html = '' + this.escapeHtml(trans.help_all_title || 'All Items') + '';
- html += '' + this.escapeHtml(trans.help_all_desc || 'Selects all items without any filtering.') + '
';
- break;
-
- case 'specific':
- html = '' + this.escapeHtml(trans.help_specific_title || 'Specific Items') + '';
- html += '' + this.escapeHtml(trans.help_specific_desc || 'Search and select individual items by name, reference, or ID.') + '
';
- break;
-
- case 'by_category':
- html = '' + this.escapeHtml(trans.help_category_title || 'By Category') + '';
- html += '' + this.escapeHtml(trans.help_category_desc || 'Select items belonging to specific categories. Includes subcategories.') + '
';
- break;
-
- case 'by_manufacturer':
- html = '' + this.escapeHtml(trans.help_manufacturer_title || 'By Manufacturer') + '';
- html += '' + this.escapeHtml(trans.help_manufacturer_desc || 'Select items from specific manufacturers/brands.') + '
';
- break;
-
- case 'by_supplier':
- html = '' + this.escapeHtml(trans.help_supplier_title || 'By Supplier') + '';
- html += '' + this.escapeHtml(trans.help_supplier_desc || 'Select items from specific suppliers.') + '
';
- break;
-
- case 'by_tag':
- html = '' + this.escapeHtml(trans.help_tag_title || 'By Tag') + '';
- html += '' + this.escapeHtml(trans.help_tag_desc || 'Select items with specific tags assigned.') + '
';
- break;
-
- case 'by_attribute':
- html = '' + this.escapeHtml(trans.help_attribute_title || 'By Attribute') + '';
- html += '' + this.escapeHtml(trans.help_attribute_desc || 'Select items with specific attribute values (e.g., Color: Red).') + '
';
- break;
-
- case 'by_feature':
- html = '' + this.escapeHtml(trans.help_feature_title || 'By Feature') + '';
- html += '' + this.escapeHtml(trans.help_feature_desc || 'Select items with specific feature values (e.g., Material: Cotton).') + '
';
- break;
-
- case 'by_combination':
- html = '' + this.escapeHtml(trans.help_combination_title || 'Combination Targeting') + '';
- html += '' + this.escapeHtml(trans.help_combination_desc || 'Select items by combination attributes.') + '
';
- html += '' + this.escapeHtml(trans.help_combination_logic || 'Logic:') + '
';
- html += '';
- html += '- ' + this.escapeHtml(trans.help_combination_within || 'Within group: OR (Red OR Blue)') + '
';
- html += '- ' + this.escapeHtml(trans.help_combination_between || 'Between groups: AND (Color AND Size)') + '
';
- html += '
';
- break;
-
- case 'by_carrier':
- html = '' + this.escapeHtml(trans.help_carrier_title || 'By Carrier') + '';
- html += '' + this.escapeHtml(trans.help_carrier_desc || 'Select items available with specific carriers.') + '
';
- break;
-
- case 'by_condition':
- html = '' + this.escapeHtml(trans.help_condition_title || 'By Condition') + '';
- html += '' + this.escapeHtml(trans.help_condition_desc || 'Filter by product condition: New, Used, or Refurbished.') + '
';
- break;
-
- case 'by_visibility':
- html = '' + this.escapeHtml(trans.help_visibility_title || 'By Visibility') + '';
- html += '' + this.escapeHtml(trans.help_visibility_desc || 'Filter by where products are visible in the store.') + '
';
- break;
-
- case 'by_active_status':
- html = '' + this.escapeHtml(trans.help_active_title || 'By Active Status') + '';
- html += '' + this.escapeHtml(trans.help_active_desc || 'Filter by whether products are enabled or disabled.') + '
';
- break;
-
- case 'by_stock_status':
- html = '' + this.escapeHtml(trans.help_stock_title || 'By Stock Status') + '';
- html += '' + this.escapeHtml(trans.help_stock_desc || 'Filter by stock availability: In stock, Out of stock, or Low stock.') + '
';
- break;
-
- case 'by_on_sale':
- case 'by_has_specific_price':
- case 'by_is_virtual':
- case 'by_is_pack':
- case 'by_has_combinations':
- case 'by_available_for_order':
- case 'by_online_only':
- case 'by_has_related':
- case 'by_has_customization':
- case 'by_has_attachments':
- case 'by_has_additional_shipping':
- html = '' + this.escapeHtml(trans.help_boolean_title || 'Yes/No Filter') + '';
- html += '' + this.escapeHtml(trans.help_boolean_desc || 'Filter products by this property.') + '
';
- break;
-
- case 'by_name_pattern':
- case 'by_reference_pattern':
- case 'by_description_pattern':
- case 'by_long_description_pattern':
- case 'by_ean13_pattern':
- case 'by_upc_pattern':
- case 'by_isbn_pattern':
- case 'by_mpn_pattern':
- case 'by_meta_title_pattern':
- case 'by_meta_description_pattern':
- html = '' + this.escapeHtml(trans.help_pattern_title || 'Pattern Matching') + '';
- html += '' + this.escapeHtml(trans.help_pattern_desc || 'Match text using patterns with wildcards.') + '
';
- html += '* ' + this.escapeHtml(trans.help_pattern_wildcard || 'any text') + '
';
- html += '{number} ' + this.escapeHtml(trans.help_pattern_number || 'any number') + '
';
- html += '{letter} ' + this.escapeHtml(trans.help_pattern_letter || 'single letter A-Z') + '
';
- break;
-
- case 'by_id_range':
- case 'by_price_range':
- case 'by_weight_range':
- case 'by_quantity_range':
- case 'by_position_range':
- html = '' + this.escapeHtml(trans.help_range_title || 'Numeric Range') + '';
- html += '' + this.escapeHtml(trans.help_range_desc || 'Filter by numeric values within specified ranges.') + '
';
- html += '' + this.escapeHtml(trans.help_range_tip || 'Leave min or max empty for open-ended ranges.') + '
';
- break;
-
- case 'by_date_added':
- case 'by_date_updated':
- html = '' + this.escapeHtml(trans.help_date_title || 'Date Range') + '';
- html += '' + this.escapeHtml(trans.help_date_desc || 'Filter by date within a specific period.') + '
';
- break;
-
- default:
- break;
- }
-
- return html;
- },
-
- /**
- * Build sort options HTML for a specific block type
- */
- buildSortOptions: function(blockType) {
- var options = [];
-
- switch (blockType) {
- case 'products':
- options = [
- { value: 'sales', label: 'Best sellers' },
- { value: 'date_add', label: 'Newest' },
- { value: 'price', label: 'Price' },
- { value: 'name', label: 'Name' },
- { value: 'position', label: 'Position' },
- { value: 'quantity', label: 'Stock quantity' },
- { value: 'random', label: 'Random' }
- ];
- break;
-
- case 'categories':
- options = [
- { value: 'name', label: 'Name' },
- { value: 'position', label: 'Position' },
- { value: 'product_count', label: 'Product count' },
- { value: 'total_sales', label: 'Best sellers' },
- { value: 'newest_products', label: 'Newest products' },
- { value: 'date_add', label: 'Creation date' },
- { value: 'random', label: 'Random' }
- ];
- break;
-
- case 'manufacturers':
- case 'suppliers':
- options = [
- { value: 'name', label: 'Name' },
- { value: 'product_count', label: 'Product count' },
- { value: 'total_sales', label: 'Best sellers' },
- { value: 'newest_products', label: 'Newest products' },
- { value: 'random', label: 'Random' }
- ];
- break;
-
- case 'cms':
- case 'cms_categories':
- options = [
- { value: 'name', label: 'Name' },
- { value: 'position', label: 'Position' },
- { value: 'random', label: 'Random' }
- ];
- break;
-
- default:
- options = [
- { value: 'name', label: 'Name' },
- { value: 'random', label: 'Random' }
- ];
- }
-
- var html = '';
- for (var i = 0; i < options.length; i++) {
- html += '';
- }
-
- return html;
- },
-
- /**
- * Update the modifier toggle button state
- */
- updateModifierButtonState: function($group) {
- var limit = $group.find('.group-modifier-limit').val();
- var sortBy = $group.find('.group-modifier-sort').val();
- var $modifiers = $group.find('.group-modifiers');
- var $btn = $group.find('.btn-toggle-modifiers');
- var trans = this.config.trans || {};
-
- $btn.find('.modifier-summary').remove();
-
- if (limit || sortBy) {
- $modifiers.addClass('has-values');
-
- var summary = [];
- if (limit) {
- summary.push((trans.top || 'Top') + ' ' + limit);
- }
- if (sortBy) {
- var sortLabel = $group.find('.group-modifier-sort option:selected').text();
- summary.push(sortLabel);
- }
-
- var $arrow = $btn.find('.toggle-arrow');
- $('' + this.escapeHtml(summary.join(', ')) + '').insertBefore($arrow);
- } else {
- $modifiers.removeClass('has-values');
- }
- },
-
- /**
- * Lock/unlock method selector when excludes are present
- */
- updateMethodSelectorLock: function($group, locked) {
- var $select = $group.find('.include-method-select');
- var $wrapper = $select.closest('.method-selector-wrapper');
- var trans = this.config.trans || {};
-
- if (locked) {
- $select.prop('disabled', true);
-
- if (!$wrapper.length) {
- $select.wrap('');
- $wrapper = $select.parent('.method-selector-wrapper');
- }
-
- $wrapper.addClass('selector-locked');
- if (!$wrapper.find('.lock-indicator').length) {
- var lockHtml = '' +
- this.esIcon('lock') +
- '' +
- (trans.remove_excludes_first || 'Remove all exceptions to change selection type') +
- '' +
- '';
- var $countEl = $wrapper.find('.condition-match-count');
- if ($countEl.length) {
- $countEl.before(lockHtml);
- } else {
- $wrapper.append(lockHtml);
- }
- }
- } else {
- $select.prop('disabled', false);
- if ($wrapper.length) {
- $wrapper.removeClass('selector-locked');
- $wrapper.find('.mpr-info-wrapper.lock-indicator').remove();
- } else {
- $select.siblings('.mpr-info-wrapper.lock-indicator').remove();
- }
- }
- }
- };
-
-})(jQuery);
-
/**
* Entity Selector - Preview Module
* Reusable preview popover component with filter and load more
@@ -8184,7 +7039,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);
@@ -8202,7 +7057,7 @@
$countValue.text(total);
} else {
// Fallback: set HTML with icon
- $totalBadge.html(self.esIcon('visibility') + ' ' + total + '');
+ $totalBadge.html(' ' + total + '');
}
$totalBadge.show();
} else {
@@ -8217,7 +7072,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);
},
@@ -8251,12 +7106,12 @@
var previewType = options.previewType || 'default';
// Build popover HTML
- var html = '';
+ var html = '
';
// Header with count and close button
html += '';
// Filter input
@@ -8284,7 +7139,7 @@
html += '';
html += '';
html += '' + (trans.of || 'of') + ' ' + remaining + ' ' + (trans.remaining || 'remaining') + '';
- html += '' + self.esIcon('add') + '';
+ html += '';
html += '
';
html += '
';
}
@@ -8346,7 +7201,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
@@ -8401,7 +7256,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
@@ -8451,7 +7306,7 @@
if (item.image) {
html += '
';
} else {
- html += '' + self.esIcon('inventory_2') + '
';
+ html += 'inventory_2
';
}
// Info section
@@ -8543,7 +7398,7 @@
$list.addClass('filtering');
// Add overlay if not exists
if (!$list.find('.filter-loading-overlay').length) {
- $list.append('' + this.esIcon('progress_activity', 'es-spin') + '
');
+ $list.append('
');
}
} else {
$list.removeClass('filtering');
@@ -8595,7 +7450,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);
@@ -8618,7 +7473,7 @@
footerHtml += '';
footerHtml += '';
footerHtml += '' + (trans.of || 'of') + ' ' + remaining + ' ' + (trans.remaining || 'remaining') + '';
- footerHtml += '' + self.esIcon('add') + '';
+ footerHtml += '';
footerHtml += ' ';
footerHtml += ' ';
@@ -8636,7 +7491,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;
@@ -8908,7 +7763,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);
}
});
@@ -9057,7 +7912,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);
}
});
@@ -9075,7 +7930,7 @@
}
if (!blockType) {
- var $block = $badge.closest('.target-block');
+ var $block = $badge.closest('.es-block');
blockType = $block.data('blockType') || 'products';
}
@@ -9412,7 +8267,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,
@@ -9428,7 +8283,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);
@@ -9440,7 +8295,7 @@
}
},
error: function() {
- $btn.prop('disabled', false).find('i').removeClass('es-spin');
+ $btn.prop('disabled', false).find('i').removeClass('icon-spin');
}
});
},
@@ -9500,13 +8355,13 @@
html += '';
html += '';
html += '
';
- html += '
' + this.esIcon('progress_activity', 'es-spin') + ' ' + (trans.loading || 'Loading...') + '
';
+ html += '
' + (trans.loading || 'Loading...') + '
';
html += '
';
html += '
';
html += '';
@@ -9579,10 +8434,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');
+ }
+ });
},
/**
@@ -9628,7 +8544,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');
@@ -9637,7 +8553,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({
@@ -9651,7 +8567,7 @@
// Build popover HTML
var totalCount = parseInt($badge.find('.count-value').text(), 10) || 0;
- var popoverHtml = '
';
+ var popoverHtml = '
';
popoverHtml += '