Files
prestashop-entity-selector/assets/js/admin/entity-list-preview.js
myprestarocks a285018e0d Initial commit: prestashop-entity-selector
Forked from prestashop-target-conditions
Renamed all references from target-conditions to entity-selector
2026-01-26 14:02:54 +00:00

1150 lines
46 KiB
JavaScript
Executable File

/**
* Target Conditions List Preview
*
* Handles showing item previews in admin list views when clicking on target count badges.
* Uses the TargetConditions trait's AJAX endpoint to load preview data.
*
* Note: Uses native event capturing to intercept clicks before PrestaShop's
* inline onclick handlers on parent td elements can navigate away.
*/
(function($) {
'use strict';
var TargetListPreview = {
$popover: null,
$backdrop: null,
currentTrigger: null,
currentConditions: null,
ajaxUrl: '',
trans: {},
currentEntityType: null,
filterQuery: '',
filterTimeout: null,
loadedData: {},
itemsPerPage: 20,
init: function(config) {
this.ajaxUrl = config.ajaxUrl || '';
this.trans = config.trans || {};
this.createPopover();
this.bindEvents();
this.loadBadgeCounts();
},
loadBadgeCounts: function() {
var self = this;
var $triggers = $('.target-preview-trigger');
if ($triggers.length === 0) {
return;
}
$triggers.each(function() {
var $trigger = $(this);
var conditions = $trigger.data('conditions');
if (!conditions) {
return;
}
if (typeof conditions === 'string') {
try {
conditions = JSON.parse(conditions);
} catch (e) {
return;
}
}
// Check for summary badge (more than 3 entity types)
var $summaryBadge = $trigger.find('.badge[data-entity-type="summary"]');
if ($summaryBadge.length > 0) {
self.fetchSummaryCount($trigger, conditions, $summaryBadge);
return;
}
// Find entity types with groups
for (var entityType in conditions) {
if (conditions[entityType] && conditions[entityType].groups && conditions[entityType].groups.length > 0) {
self.fetchEntityCount($trigger, entityType, conditions[entityType]);
}
}
});
},
fetchSummaryCount: function($trigger, conditions, $badge) {
var self = this;
var entityTypes = ($badge.data('entity-types') || '').split(',').filter(function(t) { return t; });
var pendingCount = entityTypes.length;
var totalSum = 0;
var hasAll = $badge.data('has-all') === 1 || $badge.data('has-all') === '1';
if (pendingCount === 0) {
return;
}
entityTypes.forEach(function(entityType) {
var conditionsData = conditions[entityType];
if (!conditionsData || !conditionsData.groups || conditionsData.groups.length === 0) {
pendingCount--;
if (pendingCount === 0) {
self.updateSummaryBadge($badge, totalSum, hasAll);
}
return;
}
$.ajax({
url: self.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewTargetConditions',
trait: 'TargetConditions',
block_type: entityType,
conditions: JSON.stringify(conditionsData),
limit: 0
}
}).done(function(response) {
if (response.success && typeof response.total !== 'undefined') {
totalSum += response.total;
}
}).always(function() {
pendingCount--;
if (pendingCount === 0) {
self.updateSummaryBadge($badge, totalSum, hasAll);
}
});
});
},
updateSummaryBadge: function($badge, total, hasAll) {
var displayText;
if (hasAll) {
displayText = (this.trans.multiple || 'Multiple') + ' (' + total + ')';
} else {
displayText = total + ' ' + (this.trans.items || 'items');
}
$badge.empty()
.append($('<i>', { class: 'icon-th-list' }))
.append(' ' + displayText);
$badge.attr('title', displayText);
if (total === 0) {
$badge.removeClass('badge-info badge-success badge-warning').addClass('badge-danger');
}
},
fetchEntityCount: function($trigger, entityType, conditionsData) {
var self = this;
// Skip AJAX if no badge exists for this entity type
var $badge = $trigger.find('.badge[data-entity-type="' + entityType + '"]');
if ($badge.length === 0) {
return;
}
$.ajax({
url: this.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewTargetConditions',
trait: 'TargetConditions',
block_type: entityType,
conditions: JSON.stringify(conditionsData),
limit: 0
}
}).done(function(response) {
if (response.success && typeof response.total !== 'undefined') {
self.updateBadgeCount($trigger, entityType, response.total);
}
});
},
updateBadgeCount: function($trigger, entityType, total) {
// Find badge for this specific entity type only - no fallback
var $badge = $trigger.find('.badge[data-entity-type="' + entityType + '"]');
if ($badge.length === 0) {
return;
}
var hasAll = $badge.data('has-all') === 1 || $badge.data('has-all') === '1';
var excludes = parseInt($badge.data('excludes') || 0, 10);
// Entity icon mapping
var iconMap = {
'products': 'icon-cube',
'categories': 'icon-folder-open',
'manufacturers': 'icon-building',
'suppliers': 'icon-truck',
'cms': 'icon-file-text',
'cms_categories': 'icon-folder'
};
// Get entity icon (first <i> that's not the spinner)
var $existingIcon = $badge.find('i').not('.icon-spinner').first();
var iconClass = $existingIcon.length ? $existingIcon.attr('class') : (iconMap[entityType] || 'icon-list');
var $icon = $('<i>', { class: iconClass });
// Build display text
var displayText;
var entityLabels = this.trans || {};
var singular = entityLabels[entityType + '_singular'] || entityType.replace(/_/g, ' ').replace(/s$/, '');
var plural = entityLabels[entityType + '_plural'] || entityType.replace(/_/g, ' ');
if (hasAll) {
// "All products (49)" or "All products"
var allLabel = entityLabels[entityType + '_all'] || ('All ' + plural);
displayText = allLabel + ' (' + total + ')';
} else {
// "5 products"
displayText = total + ' ' + (total === 1 ? singular : plural);
}
// Update badge content
$badge.empty().append($icon).append(' ' + displayText);
// Update tooltip
var tooltip = displayText;
if (excludes > 0) {
tooltip += ' (with ' + excludes + ' exclusion' + (excludes === 1 ? '' : 's') + ')';
}
$badge.attr('title', tooltip);
// Update color based on count
if (total === 0) {
$badge.removeClass('badge-info badge-success badge-warning').addClass('badge-danger');
}
},
createPopover: function() {
this.$popover = $('<div>', {
class: 'target-list-preview-popover',
css: { display: 'none' }
}).appendTo('body');
this.$backdrop = $('<div>', {
class: 'target-list-preview-backdrop',
css: { display: 'none' }
}).appendTo('body');
this.initDragAndResize();
},
setPopoverContent: function(html) {
this.$popover.html(html + '<div class="popover-resize-handle"></div>');
},
initDragAndResize: function() {
var self = this;
var isDragging = false;
var isResizing = false;
var startX, startY, startLeft, startTop, startWidth, startHeight;
// Drag by header
this.$popover.on('mousedown', '.preview-header', function(e) {
if ($(e.target).closest('.preview-close').length) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(self.$popover.css('left'), 10) || 0;
startTop = parseInt(self.$popover.css('top'), 10) || 0;
self.$popover.addClass('dragging');
e.preventDefault();
});
// Resize by handle
this.$popover.on('mousedown', '.popover-resize-handle', function(e) {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = self.$popover.outerWidth();
startHeight = self.$popover.outerHeight();
self.$popover.addClass('resizing');
e.preventDefault();
e.stopPropagation();
});
$(document).on('mousemove', function(e) {
if (isDragging) {
var dx = e.clientX - startX;
var dy = e.clientY - startY;
var newLeft = startLeft + dx;
var newTop = startTop + dy;
// Keep within viewport
var maxLeft = $(window).width() - self.$popover.outerWidth() - 10;
var maxTop = $(window).height() - 50;
newLeft = Math.max(10, Math.min(newLeft, maxLeft));
newTop = Math.max(10, Math.min(newTop, maxTop));
self.$popover.css({ left: newLeft, top: newTop });
}
if (isResizing) {
var dx = e.clientX - startX;
var dy = e.clientY - startY;
var newWidth = Math.max(300, Math.min(startWidth + dx, $(window).width() - 40));
var newHeight = Math.max(200, Math.min(startHeight + dy, $(window).height() - 40));
self.$popover.css({
width: newWidth,
height: newHeight
});
}
});
$(document).on('mouseup', function() {
if (isDragging) {
isDragging = false;
self.$popover.removeClass('dragging');
}
if (isResizing) {
isResizing = false;
self.$popover.removeClass('resizing');
}
});
},
bindEvents: function() {
var self = this;
// Use native event listener with capture phase to intercept BEFORE
// the inline onclick on parent td elements can fire
document.addEventListener('click', function(e) {
var trigger = e.target.closest('.target-preview-trigger');
if (trigger) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// Also remove inline onclick from parent td to prevent navigation
var td = trigger.closest('td');
if (td && td.onclick) {
td._originalOnclick = td.onclick;
td.onclick = null;
// Restore after a tick
setTimeout(function() {
if (td._originalOnclick) {
td.onclick = td._originalOnclick;
delete td._originalOnclick;
}
}, 0);
}
self.showPreview($(trigger));
return false;
}
}, true); // true = capture phase
// Click backdrop to close
this.$backdrop.on('click', function() {
self.hidePreview();
});
// ESC to close
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && self.$popover.is(':visible')) {
self.hidePreview();
}
});
// Close button
this.$popover.on('click', '.preview-close', function() {
self.hidePreview();
});
// Tab switching
this.$popover.on('click', '.preview-tab', function() {
var $tab = $(this);
var entityType = $tab.data('entity');
self.$popover.find('.preview-tab').removeClass('active');
$tab.addClass('active');
self.$popover.find('.preview-content').removeClass('active');
self.$popover.find('.preview-content[data-entity="' + entityType + '"]').addClass('active');
self.currentEntityType = entityType;
self.filterQuery = '';
self.$popover.find('.preview-filter-input').val('');
});
// Filter input with debounce
this.$popover.on('input', '.preview-filter-input', function() {
var query = $(this).val().trim().toLowerCase();
clearTimeout(self.filterTimeout);
self.filterTimeout = setTimeout(function() {
self.filterQuery = query;
self.applyFilter();
}, 150);
});
// Load more button click
this.$popover.on('click', '.btn-load-more', function() {
var $content = $(this).closest('.preview-content');
var entityType = $content.data('entity');
var limit = parseInt($content.find('.load-more-select').val(), 10);
self.loadMoreItems(entityType, limit);
});
// Rule badge click - show entity names
this.$popover.on('click', '.preview-rule', function(e) {
e.stopPropagation();
var $rule = $(this);
// If clicking on same rule with open popup, close it
if (self.$currentRuleElement && self.$currentRuleElement.is($rule)) {
$('.rule-detail-popup').remove();
self.$currentRuleElement = null;
self.$currentRulePopup = null;
return;
}
self.showRuleDetails($rule);
});
// Close rule popup when clicking elsewhere
$(document).on('click', function(e) {
if (!$(e.target).closest('.preview-rule').length && !$(e.target).closest('.rule-detail-popup').length) {
$('.rule-detail-popup').remove();
self.$currentRuleElement = null;
self.$currentRulePopup = null;
}
});
},
showRuleDetails: function($rule) {
var self = this;
var entityType = $rule.data('entity-type');
var method = $rule.data('method');
var values = $rule.data('values');
// Methods that don't have entity IDs to resolve
var nonEntityMethods = ['all', 'by_price_range', 'by_quantity_range', 'by_condition',
'by_visibility', 'by_active_status', 'by_stock_status', 'by_on_sale', 'by_is_virtual',
'by_is_pack', 'by_has_combinations', 'by_name_pattern', 'by_reference_pattern',
'by_date_added', 'by_date_updated', 'by_weight_range', 'by_depth', 'by_active',
'by_newsletter', 'by_optin'];
if (nonEntityMethods.indexOf(method) !== -1 || !entityType || !values || values.length === 0) {
// Show simple info popup with properly formatted values
var valuesDisplay = this.formatValuesForDisplay(values);
this.showRulePopup($rule, '<div class="rule-detail-info">' +
'<strong>' + this.humanizeMethod(method) + '</strong>' +
(valuesDisplay ? '<br><small>' + valuesDisplay + '</small>' : '') +
'</div>');
return;
}
// Show loading state
this.showRulePopup($rule, '<div class="rule-detail-loading"><i class="icon-spinner icon-spin"></i> Loading...</div>');
// Fetch entity names
$.ajax({
url: this.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getTargetEntitiesByIds',
trait: 'TargetConditions',
entity_type: entityType,
ids: JSON.stringify(values)
}
}).done(function(response) {
if (response.success && response.entities) {
var icon = self.getEntityIcon(entityType);
var html = '<div class="rule-detail-list">';
response.entities.forEach(function(entity) {
html += '<div class="rule-detail-item">';
if (entity.image) {
html += '<img src="' + entity.image + '" class="rule-detail-img">';
} else {
html += '<span class="rule-detail-icon"><i class="' + icon + '"></i></span>';
}
html += '<span>' + self.escapeHtml(entity.name || entity.title || 'ID: ' + entity.id) + '</span>';
html += '</div>';
});
html += '</div>';
self.updateRulePopup(html);
} else {
self.updateRulePopup('<div class="rule-detail-error">Failed to load names</div>');
}
}).fail(function() {
self.updateRulePopup('<div class="rule-detail-error">Failed to load names</div>');
});
},
formatValuesForDisplay: function(values) {
if (!values || values.length === 0) {
return '';
}
var formatted = [];
for (var i = 0; i < values.length && i < 10; i++) {
var val = values[i];
if (typeof val === 'object' && val !== null) {
// Handle object values (like {min: x, max: y} for ranges)
if (val.min !== undefined || val.max !== undefined) {
formatted.push((val.min || '0') + ' - ' + (val.max || '∞'));
} else if (val.pattern !== undefined) {
formatted.push('"' + val.pattern + '"');
} else if (val.from !== undefined || val.to !== undefined) {
formatted.push((val.from || 'start') + ' → ' + (val.to || 'now'));
} else {
// Generic object - show key: value pairs
var parts = [];
for (var key in val) {
if (val.hasOwnProperty(key)) {
parts.push(key + ': ' + val[key]);
}
}
formatted.push(parts.join(', '));
}
} else {
formatted.push(String(val));
}
}
var result = formatted.join(', ');
if (values.length > 10) {
result += ' (+' + (values.length - 10) + ' more)';
}
return result;
},
showRulePopup: function($rule, content) {
// Remove any existing popup
$('.rule-detail-popup').remove();
// Create popup and append to body for proper positioning
var $popup = $('<div>', { class: 'rule-detail-popup' }).html(content);
$('body').append($popup);
// Position below the rule badge
var offset = $rule.offset();
var ruleHeight = $rule.outerHeight();
var popupWidth = $popup.outerWidth();
var windowWidth = $(window).width();
var left = offset.left;
var top = offset.top + ruleHeight + 4;
// Keep popup within viewport
if (left + popupWidth > windowWidth - 10) {
left = windowWidth - popupWidth - 10;
}
if (left < 10) {
left = 10;
}
$popup.css({
position: 'fixed',
top: top - $(window).scrollTop(),
left: left
});
this.$currentRulePopup = $popup;
this.$currentRuleElement = $rule;
},
updateRulePopup: function(content) {
if (this.$currentRulePopup) {
this.$currentRulePopup.html(content);
}
},
showPreview: function($trigger) {
var self = this;
var conditions = $trigger.data('conditions');
if (!conditions) {
return;
}
if (typeof conditions === 'string') {
try {
conditions = JSON.parse(conditions);
} catch (e) {
console.error('[TargetListPreview] Failed to parse conditions:', e);
return;
}
}
this.currentTrigger = $trigger;
this.currentConditions = conditions;
this.loadedData = {};
this.filterQuery = '';
// Show loading state
this.setPopoverContent(this.renderLoading());
this.$popover.show();
this.$backdrop.show();
// Position popover
this.positionPopover();
// Load preview data
this.loadPreview(conditions);
},
hidePreview: function() {
this.$popover.hide();
this.$backdrop.hide();
this.currentTrigger = null;
this.currentConditions = null;
this.currentEntityType = null;
// Clean up any open rule popups
$('.rule-detail-popup').remove();
this.$currentRuleElement = null;
this.$currentRulePopup = null;
},
positionPopover: function() {
if (!this.currentTrigger) return;
var $trigger = this.currentTrigger;
var offset = $trigger.offset();
var triggerWidth = $trigger.outerWidth();
var triggerHeight = $trigger.outerHeight();
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var scrollTop = $(window).scrollTop();
var scrollLeft = $(window).scrollLeft();
var popoverWidth = this.$popover.outerWidth() || 380;
var popoverHeight = this.$popover.outerHeight() || 500;
// Convert document offset to viewport position (for position: fixed)
var triggerViewportLeft = offset.left - scrollLeft;
var triggerViewportTop = offset.top - scrollTop;
// Center popover below trigger
var left = triggerViewportLeft + (triggerWidth / 2) - (popoverWidth / 2);
var top = triggerViewportTop + triggerHeight + 8;
// Keep within viewport bounds
if (left < 10) left = 10;
if (left + popoverWidth > windowWidth - 10) {
left = windowWidth - popoverWidth - 10;
}
// Calculate space available above and below
var spaceBelow = windowHeight - (triggerViewportTop + triggerHeight + 8);
var spaceAbove = triggerViewportTop - 8;
// Only flip to above if:
// 1. Not enough space below for popover AND
// 2. More space above than below
if (spaceBelow < popoverHeight && spaceAbove > spaceBelow) {
top = triggerViewportTop - popoverHeight - 8;
// Ensure not above viewport
if (top < 10) top = 10;
} else {
// Show below, constrain to viewport if needed
if (top + popoverHeight > windowHeight - 10) {
top = windowHeight - popoverHeight - 10;
}
}
this.$popover.css({ left: left, top: top });
},
loadPreview: function(conditions) {
var self = this;
var entityTypes = [];
for (var type in conditions) {
if (conditions[type] && conditions[type].groups && conditions[type].groups.length > 0) {
entityTypes.push(type);
}
}
if (entityTypes.length === 0) {
this.setPopoverContent(this.renderEmpty());
return;
}
var promises = [];
var results = {};
entityTypes.forEach(function(entityType) {
var promise = $.ajax({
url: self.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewTargetConditions',
trait: 'TargetConditions',
block_type: entityType,
conditions: JSON.stringify(conditions[entityType]),
limit: self.itemsPerPage
}
}).then(function(response) {
results[entityType] = response;
self.loadedData[entityType] = {
items: response.items || [],
total: response.total || 0,
offset: response.items ? response.items.length : 0
};
});
promises.push(promise);
});
$.when.apply($, promises).then(function() {
self.currentEntityType = entityTypes[0];
self.setPopoverContent(self.renderPreview(entityTypes, results));
self.positionPopover();
}).fail(function() {
self.setPopoverContent(self.renderError());
});
},
loadMoreItems: function(entityType, limit) {
var self = this;
var data = this.loadedData[entityType];
if (!data || data.offset >= data.total) return;
var requestLimit = limit || this.itemsPerPage;
var $content = this.$popover.find('.preview-content[data-entity="' + entityType + '"]');
var $loadMore = $content.find('.preview-footer');
$loadMore.html('<span class="loading-text"><i class="icon-spinner icon-spin"></i> ' + (this.trans.loading || 'Loading...') + '</span>');
$.ajax({
url: this.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewTargetConditions',
trait: 'TargetConditions',
block_type: entityType,
conditions: JSON.stringify(this.currentConditions[entityType]),
limit: requestLimit,
offset: data.offset
}
}).done(function(response) {
if (response.success && response.items) {
data.items = data.items.concat(response.items);
data.offset += response.items.length;
var $list = $content.find('.preview-items');
response.items.forEach(function(item) {
$list.append(self.renderItem(item));
});
self.updateFooter(entityType);
self.applyFilter();
}
}).fail(function() {
$loadMore.html('<span class="error-text"><i class="icon-warning"></i> ' + (self.trans.error || 'Failed to load') + '</span>');
});
},
applyFilter: function() {
var query = this.filterQuery;
var $content = this.$popover.find('.preview-content.active');
var $items = $content.find('.preview-item');
if (!query) {
$items.show();
return;
}
$items.each(function() {
var $item = $(this);
var name = ($item.find('.preview-item-name').text() || '').toLowerCase();
var ref = ($item.find('.preview-item-ref').text() || '').toLowerCase();
if (name.indexOf(query) !== -1 || ref.indexOf(query) !== -1) {
$item.show();
} else {
$item.hide();
}
});
},
updateFooter: function(entityType) {
var data = this.loadedData[entityType];
var $content = this.$popover.find('.preview-content[data-entity="' + entityType + '"]');
var $footer = $content.find('.preview-footer');
if (data.offset >= data.total) {
$footer.remove();
} else {
var remaining = data.total - data.offset;
var html = '<div class="load-more-controls">';
html += '<span class="load-more-label">Load</span>';
html += '<select class="load-more-select">';
if (remaining >= 10) html += '<option value="10">10</option>';
if (remaining >= 20) html += '<option value="20">20</option>';
if (remaining >= 50) html += '<option value="50">50</option>';
if (remaining >= 100) html += '<option value="100">100</option>';
html += '<option value="' + remaining + '" data-all="true">All (' + remaining + ')</option>';
html += '</select>';
html += '<span class="load-more-of">of <span class="remaining-count">' + remaining + '</span> remaining</span>';
html += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
html += '</div>';
$footer.html(html);
}
},
renderLoading: function() {
return '<div class="preview-loading">' +
'<i class="icon-spinner icon-spin"></i> ' +
(this.trans.loading || 'Loading...') +
'</div>';
},
renderEmpty: function() {
return '<div class="preview-header">' +
'<span class="preview-title">' + (this.trans.items_preview || 'Items Preview') + '</span>' +
'<button type="button" class="preview-close"><i class="icon-times"></i></button>' +
'</div>' +
'<div class="preview-empty">' +
'<i class="icon-info-circle"></i> ' +
(this.trans.no_items || 'No items selected') +
'</div>';
},
renderError: function() {
return '<div class="preview-header">' +
'<span class="preview-title">' + (this.trans.items_preview || 'Items Preview') + '</span>' +
'<button type="button" class="preview-close"><i class="icon-times"></i></button>' +
'</div>' +
'<div class="preview-error">' +
'<i class="icon-warning"></i> ' +
(this.trans.error || 'Failed to load preview') +
'</div>';
},
renderPreview: function(entityTypes, results) {
var self = this;
var html = '<div class="preview-header">';
html += '<span class="preview-title">' + (this.trans.items_preview || 'Items Preview') + '</span>';
html += '<button type="button" class="preview-close"><i class="icon-times"></i></button>';
html += '</div>';
// Tabs if multiple entity types
if (entityTypes.length > 1) {
html += '<div class="preview-tabs">';
entityTypes.forEach(function(type, index) {
var result = results[type];
var count = result && result.total ? result.total : 0;
var icon = self.getEntityIcon(type);
var label = self.getEntityLabel(type);
var activeClass = index === 0 ? ' active' : '';
html += '<button type="button" class="preview-tab' + activeClass + '" data-entity="' + type + '" title="' + label + '">';
html += '<i class="' + icon + '"></i> ' + count;
html += '</button>';
});
html += '</div>';
}
// Filter input
html += '<div class="preview-filter">';
html += '<i class="icon-search"></i>';
html += '<input type="text" class="preview-filter-input" placeholder="' + (this.trans.filter || 'Filter...') + '">';
html += '</div>';
// Content for each entity type
html += '<div class="preview-contents">';
entityTypes.forEach(function(type, index) {
var activeClass = index === 0 ? ' active' : '';
html += '<div class="preview-content' + activeClass + '" data-entity="' + type + '">';
html += self.renderEntityPreview(type, results[type]);
html += '</div>';
});
html += '</div>';
return html;
},
renderEntityPreview: function(entityType, result) {
if (!result || !result.success) {
return '<div class="preview-error-inline"><i class="icon-warning"></i> ' + (this.trans.error || 'Failed to load') + '</div>';
}
var items = result.items || [];
var total = result.total || 0;
var self = this;
var html = '';
// Render rules summary if conditions are available
var conditions = this.currentConditions && this.currentConditions[entityType];
if (conditions && conditions.groups && conditions.groups.length > 0) {
html += this.renderRulesSummary(entityType, conditions.groups);
}
if (items.length === 0) {
return html + '<div class="preview-empty-inline"><i class="icon-info-circle"></i> ' + (this.trans.no_items || 'No items') + '</div>';
}
html += '<div class="preview-items">';
items.forEach(function(item) {
html += self.renderItem(item);
});
html += '</div>';
if (total > items.length) {
var remaining = total - items.length;
html += '<div class="preview-footer">';
html += '<div class="load-more-controls">';
html += '<span class="load-more-label">Load</span>';
html += '<select class="load-more-select">';
if (remaining >= 10) html += '<option value="10">10</option>';
if (remaining >= 20) html += '<option value="20">20</option>';
if (remaining >= 50) html += '<option value="50">50</option>';
if (remaining >= 100) html += '<option value="100">100</option>';
html += '<option value="' + remaining + '" data-all="true">All (' + remaining + ')</option>';
html += '</select>';
html += '<span class="load-more-of">of <span class="remaining-count">' + remaining + '</span> remaining</span>';
html += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
html += '</div>';
html += '</div>';
}
return html;
},
renderRulesSummary: function(entityType, groups) {
var self = this;
var html = '<div class="preview-rules">';
groups.forEach(function(group, index) {
var include = group.include || {};
var excludes = group.excludes || [];
var method = include.method || 'unknown';
var values = include.values || [];
html += '<div class="preview-rule-group">';
// Group label
if (groups.length > 1) {
html += '<span class="preview-group-label">Group ' + (index + 1) + '</span>';
}
// Include rule - clickable to show details
var entityTypeForMethod = self.getEntityTypeForMethod(entityType, method);
html += '<div class="preview-rule include" ';
html += 'data-method="' + self.escapeAttr(method) + '" ';
html += 'data-entity-type="' + self.escapeAttr(entityTypeForMethod) + '" ';
html += 'data-values="' + self.escapeAttr(JSON.stringify(values)) + '">';
html += '<span class="rule-icon"><i class="icon-check"></i></span>';
html += '<span class="rule-text">' + self.formatRuleMethod(method, values.length) + '</span>';
html += '</div>';
// Exclusion rules
if (excludes.length > 0) {
excludes.forEach(function(exclude) {
var exMethod = exclude.method || 'unknown';
var exValues = exclude.values || [];
var exEntityType = self.getEntityTypeForMethod(entityType, exMethod);
html += '<div class="preview-rule exclude" ';
html += 'data-method="' + self.escapeAttr(exMethod) + '" ';
html += 'data-entity-type="' + self.escapeAttr(exEntityType) + '" ';
html += 'data-values="' + self.escapeAttr(JSON.stringify(exValues)) + '">';
html += '<span class="rule-icon"><i class="icon-minus"></i></span>';
html += '<span class="rule-text">' + self.formatRuleMethod(exMethod, exValues.length) + '</span>';
html += '</div>';
});
}
html += '</div>';
});
html += '</div>';
return html;
},
getEntityTypeForMethod: function(baseEntityType, method) {
// Map method to entity type for resolving names
var methodToEntity = {
'specific': baseEntityType,
'by_category': 'categories',
'by_manufacturer': 'manufacturers',
'by_supplier': 'suppliers',
'by_attribute': 'attributes',
'by_feature': 'features',
'by_parent': 'categories',
'by_cms_category': 'cms_categories',
'by_group': 'customer_groups',
'by_profile': 'profiles',
'by_zone': 'zones',
'by_country': 'countries'
};
return methodToEntity[method] || null;
},
buildRuleTooltip: function(type, method, values) {
var tooltip = type + ': ' + this.humanizeMethod(method);
if (values && values.length > 0) {
tooltip += ' (' + values.length + ' selected)';
}
return tooltip;
},
escapeAttr: function(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},
formatRuleMethod: function(method, valueCount) {
var countStr = valueCount !== undefined ? ' (' + valueCount + ')' : '';
var methodLabels = {
// Basic methods
'all': 'All',
'specific': 'Specific' + countStr,
// Product selection methods
'by_category': 'Category' + countStr,
'by_manufacturer': 'Manufacturer' + countStr,
'by_supplier': 'Supplier' + countStr,
'by_attribute': 'Attribute' + countStr,
'by_feature': 'Feature' + countStr,
'by_price_range': 'Price range',
'by_quantity_range': 'Quantity range',
'by_condition': 'Condition' + countStr,
'by_visibility': 'Visibility' + countStr,
'by_active_status': 'Active status',
'by_stock_status': 'Stock status',
'by_on_sale': 'On sale',
'by_is_virtual': 'Virtual products',
'by_is_pack': 'Pack products',
'by_has_combinations': 'Has combinations',
'by_name_pattern': 'Name pattern',
'by_reference_pattern': 'Reference pattern',
'by_date_added': 'Date added',
'by_date_updated': 'Date updated',
'by_weight_range': 'Weight range',
// Category methods
'by_parent': 'Parent category' + countStr,
'by_depth': 'Depth level',
'by_active': 'Active status',
// CMS methods
'by_cms_category': 'CMS category' + countStr,
// Customer methods
'by_group': 'Customer group' + countStr,
'by_newsletter': 'Newsletter',
'by_optin': 'Opt-in',
// Employee methods
'by_profile': 'Profile' + countStr,
// Generic
'by_zone': 'Zone' + countStr,
'by_country': 'Country' + countStr
};
return methodLabels[method] || this.humanizeMethod(method) + countStr;
},
humanizeMethod: function(method) {
return method
.replace(/^by_/, '')
.replace(/_/g, ' ')
.replace(/\b\w/g, function(l) { return l.toUpperCase(); });
},
renderItem: function(item) {
var html = '<div class="preview-item">';
if (item.image) {
html += '<img src="' + item.image + '" alt="" class="preview-item-image">';
} else {
html += '<span class="preview-item-no-image"><i class="icon-picture-o"></i></span>';
}
html += '<div class="preview-item-info">';
html += '<span class="preview-item-name">' + this.escapeHtml(item.name || item.title || 'Item #' + item.id) + '</span>';
if (item.reference) {
html += '<span class="preview-item-ref">' + this.escapeHtml(item.reference) + '</span>';
}
if (item.price_formatted) {
html += '<span class="preview-item-price">' + item.price_formatted + '</span>';
}
html += '</div>';
html += '</div>';
return html;
},
escapeHtml: function(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
getEntityIcon: function(entityType) {
var icons = {
'products': 'icon-cube',
'categories': 'icon-folder-open',
'manufacturers': 'icon-building',
'suppliers': 'icon-truck',
'cms': 'icon-file-text',
'cms_categories': 'icon-folder',
'carriers': 'icon-truck',
'zones': 'icon-globe',
'countries': 'icon-flag',
'currencies': 'icon-money',
'languages': 'icon-language',
'customer_groups': 'icon-users',
'shops': 'icon-shopping-cart',
'shop_groups': 'icon-th',
'customers': 'icon-user',
'employees': 'icon-user-secret',
'profiles': 'icon-id-card',
'order_states': 'icon-check-circle',
'taxes': 'icon-percent'
};
return icons[entityType] || 'icon-list';
},
getEntityLabel: function(entityType) {
var labels = {
'products': this.trans.products || 'Products',
'categories': this.trans.categories || 'Categories',
'manufacturers': this.trans.manufacturers || 'Manufacturers',
'suppliers': this.trans.suppliers || 'Suppliers',
'cms': this.trans.cms_pages || 'CMS Pages',
'cms_categories': this.trans.cms_categories || 'CMS Categories',
'carriers': this.trans.carriers || 'Carriers',
'zones': this.trans.zones || 'Zones',
'countries': this.trans.countries || 'Countries',
'currencies': this.trans.currencies || 'Currencies',
'languages': this.trans.languages || 'Languages',
'customer_groups': this.trans.customer_groups || 'Customer Groups',
'shops': this.trans.shops || 'Shops',
'shop_groups': this.trans.shop_groups || 'Shop Groups',
'customers': this.trans.customers || 'Customers',
'employees': this.trans.employees || 'Employees',
'profiles': this.trans.profiles || 'Profiles',
'order_states': this.trans.order_states || 'Order States',
'taxes': this.trans.taxes || 'Taxes'
};
return labels[entityType] || entityType;
}
};
// Initialize when config is available
$(document).ready(function() {
if (typeof targetListPreviewConfig !== 'undefined') {
TargetListPreview.init(targetListPreviewConfig);
}
});
window.TargetListPreview = TargetListPreview;
})(jQuery);