Add hierarchical tree view for category selection
Features: - Tree view mode for categories with expand/collapse - Product count badges with clickable preview popover - Select parent with all children button - Client-side tree filtering (refine search) - Keyboard shortcuts: Ctrl+A (select all), Ctrl+D (clear) - View mode switching between tree/list/columns - Tree view as default for categories, respects user preference Backend: - Add previewCategoryProducts and previewCategoryPages AJAX handlers - Support pagination and filtering in category previews Styling: - Consistent count-badge styling across tree and other views - Loading and popover-open states for count badges Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,48 +89,190 @@
|
||||
|
||||
updateChipsVisibility: function($chips) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
var $picker = $chips.closest('.value-picker');
|
||||
var $allChips = $chips.find('.entity-chip');
|
||||
var totalCount = $allChips.length;
|
||||
var $toggle = $chips.find('.chips-show-more-toggle');
|
||||
var isExpanded = $chips.hasClass('chips-expanded');
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
// Remove existing toggle if present
|
||||
$toggle.remove();
|
||||
|
||||
if (totalCount <= this.maxVisibleChips) {
|
||||
// All chips visible, no toggle needed
|
||||
$allChips.removeClass('chip-hidden');
|
||||
$chips.removeClass('chips-expanded chips-collapsed');
|
||||
// If no chips, remove the wrapper entirely
|
||||
var $existingWrapper = $chips.closest('.chips-wrapper');
|
||||
if (totalCount === 0) {
|
||||
if ($existingWrapper.length) {
|
||||
// Move chips out of wrapper before removing
|
||||
$existingWrapper.before($chips);
|
||||
$existingWrapper.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We have more than maxVisibleChips
|
||||
var hiddenCount = totalCount - this.maxVisibleChips;
|
||||
// Ensure chips wrapper structure exists
|
||||
this.ensureChipsWrapper($chips);
|
||||
|
||||
if (isExpanded) {
|
||||
// Show all chips
|
||||
$allChips.removeClass('chip-hidden');
|
||||
var $wrapper = $chips.closest('.chips-wrapper');
|
||||
var $toolbar = $wrapper.find('.chips-toolbar');
|
||||
var $loadMore = $wrapper.find('.chips-load-more');
|
||||
|
||||
// Add collapse toggle
|
||||
var collapseText = trans.show_less || 'Show less';
|
||||
$chips.append('<span class="chips-show-more-toggle chips-collapse-toggle">' +
|
||||
'<i class="icon-chevron-up"></i> ' + collapseText + '</span>');
|
||||
} else {
|
||||
// Hide chips beyond maxVisibleChips
|
||||
$allChips.each(function(index) {
|
||||
if (index >= self.maxVisibleChips) {
|
||||
$(this).addClass('chip-hidden');
|
||||
// Get current search filter
|
||||
var searchTerm = $toolbar.find('.chips-search-input').val() || '';
|
||||
searchTerm = searchTerm.toLowerCase().trim();
|
||||
|
||||
// Filter and paginate chips
|
||||
var visibleCount = 0;
|
||||
var filteredCount = 0;
|
||||
var isExpanded = $chips.hasClass('chips-expanded');
|
||||
var maxVisible = isExpanded ? 999999 : (this.maxVisibleChips || 12);
|
||||
|
||||
$allChips.each(function() {
|
||||
var $chip = $(this);
|
||||
var chipName = ($chip.find('.chip-name').text() || '').toLowerCase();
|
||||
var matchesFilter = !searchTerm || chipName.indexOf(searchTerm) !== -1;
|
||||
|
||||
$chip.removeClass('chip-filtered-out chip-paginated-out');
|
||||
|
||||
if (!matchesFilter) {
|
||||
$chip.addClass('chip-filtered-out');
|
||||
} else {
|
||||
filteredCount++;
|
||||
if (filteredCount > maxVisible) {
|
||||
$chip.addClass('chip-paginated-out');
|
||||
} else {
|
||||
$(this).removeClass('chip-hidden');
|
||||
visibleCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update toolbar (always show when we have chips)
|
||||
$toolbar.addClass('has-chips');
|
||||
this.updateChipsToolbar($toolbar, totalCount, filteredCount, searchTerm);
|
||||
|
||||
// Update load more button
|
||||
var hiddenByPagination = filteredCount - visibleCount;
|
||||
if (hiddenByPagination > 0 && !isExpanded) {
|
||||
var moreText = (trans.show_more || 'Show {count} more').replace('{count}', hiddenByPagination);
|
||||
$loadMore.html(
|
||||
'<button type="button" class="btn-load-more">' +
|
||||
'<i class="icon-chevron-down"></i> ' + moreText +
|
||||
'</button>'
|
||||
).show();
|
||||
} else if (isExpanded && filteredCount > (this.maxVisibleChips || 12)) {
|
||||
var lessText = trans.show_less || 'Show less';
|
||||
$loadMore.html(
|
||||
'<button type="button" class="btn-load-more">' +
|
||||
'<i class="icon-chevron-up"></i> ' + lessText +
|
||||
'</button>'
|
||||
).show();
|
||||
} else {
|
||||
$loadMore.hide();
|
||||
}
|
||||
},
|
||||
|
||||
ensureChipsWrapper: function($chips) {
|
||||
// Check if already wrapped
|
||||
if ($chips.closest('.chips-wrapper').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var trans = this.config.trans || {};
|
||||
var $picker = $chips.closest('.value-picker');
|
||||
|
||||
// Create wrapper structure - simple inline toolbar
|
||||
var wrapperHtml = '<div class="chips-wrapper">' +
|
||||
'<div class="chips-toolbar">' +
|
||||
'<i class="icon-search"></i>' +
|
||||
'<input type="text" class="chips-search-input" placeholder="' + (trans.filter || 'Filter') + '...">' +
|
||||
'<span class="chips-count"></span>' +
|
||||
'<button type="button" class="btn-chips-clear" title="' + (trans.clear_all || 'Clear all') + '">' +
|
||||
'<i class="icon-trash"></i> <span class="clear-text">' + (trans.clear_all || 'Clear all') + '</span>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="chips-load-more" style="display:none;"></div>' +
|
||||
'</div>';
|
||||
|
||||
var $wrapper = $(wrapperHtml);
|
||||
|
||||
// Insert wrapper before chips and move chips inside
|
||||
$chips.before($wrapper);
|
||||
$wrapper.find('.chips-toolbar').after($chips);
|
||||
$wrapper.append($wrapper.find('.chips-load-more'));
|
||||
|
||||
// Bind toolbar events
|
||||
this.bindChipsToolbarEvents($wrapper);
|
||||
},
|
||||
|
||||
bindChipsToolbarEvents: function($wrapper) {
|
||||
var self = this;
|
||||
var $chips = $wrapper.find('.entity-chips');
|
||||
var searchTimeout;
|
||||
|
||||
// Search input
|
||||
$wrapper.on('input', '.chips-search-input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(function() {
|
||||
// Collapse when searching to show filtered results from start
|
||||
$chips.removeClass('chips-expanded');
|
||||
self.updateChipsVisibility($chips);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Clear all button
|
||||
$wrapper.on('click', '.btn-chips-clear', function() {
|
||||
var searchTerm = $wrapper.find('.chips-search-input').val() || '';
|
||||
var $chipsToRemove;
|
||||
|
||||
if (searchTerm.trim()) {
|
||||
// Remove only filtered (visible) chips
|
||||
$chipsToRemove = $chips.find('.entity-chip:not(.chip-filtered-out)');
|
||||
} else {
|
||||
// Remove all chips
|
||||
$chipsToRemove = $chips.find('.entity-chip');
|
||||
}
|
||||
|
||||
$chipsToRemove.each(function() {
|
||||
$(this).find('.chip-remove').trigger('click');
|
||||
});
|
||||
|
||||
// Add expand toggle
|
||||
var moreText = (trans.show_more || 'Show {count} more').replace('{count}', hiddenCount);
|
||||
$chips.addClass('chips-collapsed').removeClass('chips-expanded');
|
||||
$chips.append('<span class="chips-show-more-toggle chips-expand-toggle">' +
|
||||
'<i class="icon-chevron-down"></i> ' + moreText + '</span>');
|
||||
// Clear search
|
||||
$wrapper.find('.chips-search-input').val('');
|
||||
self.updateChipsVisibility($chips);
|
||||
});
|
||||
|
||||
// Load more / show less
|
||||
$wrapper.on('click', '.btn-load-more', function() {
|
||||
if ($chips.hasClass('chips-expanded')) {
|
||||
$chips.removeClass('chips-expanded');
|
||||
} else {
|
||||
$chips.addClass('chips-expanded');
|
||||
}
|
||||
self.updateChipsVisibility($chips);
|
||||
});
|
||||
},
|
||||
|
||||
updateChipsToolbar: function($toolbar, totalCount, filteredCount, searchTerm) {
|
||||
var trans = this.config.trans || {};
|
||||
var $count = $toolbar.find('.chips-count');
|
||||
var $clearBtn = $toolbar.find('.btn-chips-clear');
|
||||
var $clearText = $clearBtn.find('.clear-text');
|
||||
|
||||
// Update count display
|
||||
if (searchTerm) {
|
||||
$count.addClass('has-filter').html(
|
||||
'<span class="count-filtered">' + filteredCount + '</span>' +
|
||||
'<span class="count-separator">/</span>' +
|
||||
'<span class="count-total">' + totalCount + '</span>'
|
||||
);
|
||||
$clearText.text((trans.clear || 'Clear') + ' ' + filteredCount);
|
||||
} else {
|
||||
$count.removeClass('has-filter').html(totalCount);
|
||||
$clearText.text(trans.clear_all || 'Clear all');
|
||||
}
|
||||
|
||||
// Show/hide clear button
|
||||
if (searchTerm && filteredCount === 0) {
|
||||
$clearBtn.hide();
|
||||
} else if (totalCount > 0) {
|
||||
$clearBtn.show();
|
||||
} else {
|
||||
$clearBtn.hide();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -172,7 +314,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Now load all entities in bulk for each entity type
|
||||
// Build bulk request: { entityType: [uniqueIds], ... }
|
||||
var bulkRequest = {};
|
||||
var hasEntities = false;
|
||||
|
||||
Object.keys(entitiesToLoad).forEach(function(entityType) {
|
||||
var data = entitiesToLoad[entityType];
|
||||
if (data.ids.length === 0) return;
|
||||
@@ -182,69 +327,85 @@
|
||||
return arr.indexOf(id) === index;
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: self.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getTargetEntitiesByIds',
|
||||
trait: 'EntitySelector',
|
||||
entity_type: entityType,
|
||||
ids: JSON.stringify(uniqueIds)
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.entities) {
|
||||
// Build a map of id -> entity for quick lookup
|
||||
var entityMap = {};
|
||||
response.entities.forEach(function(entity) {
|
||||
entityMap[entity.id] = entity;
|
||||
});
|
||||
bulkRequest[entityType] = uniqueIds;
|
||||
hasEntities = true;
|
||||
});
|
||||
|
||||
// Update each picker that requested this entity type
|
||||
data.pickers.forEach(function(pickerData) {
|
||||
var $picker = pickerData.$picker;
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
|
||||
var validIds = [];
|
||||
// Skip AJAX if no entities to load
|
||||
if (!hasEntities) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace loading chips with real data
|
||||
pickerData.ids.forEach(function(id) {
|
||||
var $loadingChip = $chips.find('.entity-chip-loading[data-id="' + id + '"]');
|
||||
if (entityMap[id]) {
|
||||
var entity = entityMap[id];
|
||||
validIds.push(entity.id);
|
||||
|
||||
// Create real chip
|
||||
var html = '<span class="entity-chip" data-id="' + self.escapeAttr(entity.id) + '">';
|
||||
if (entity.image) {
|
||||
html += '<span class="chip-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
|
||||
}
|
||||
html += '<span class="chip-name">' + self.escapeHtml(entity.name) + '</span>';
|
||||
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
|
||||
html += '</span>';
|
||||
|
||||
$loadingChip.replaceWith(html);
|
||||
} else {
|
||||
// Entity not found, remove loading chip
|
||||
$loadingChip.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Update chips visibility
|
||||
self.updateChipsVisibility($chips);
|
||||
|
||||
// If some entities were not found, update the hidden input
|
||||
if (validIds.length !== pickerData.ids.length) {
|
||||
$dataInput.val(JSON.stringify(validIds));
|
||||
self.serializeAllBlocks();
|
||||
}
|
||||
|
||||
self.updateBlockStatus($picker.closest('.target-block'));
|
||||
});
|
||||
}
|
||||
// Single bulk AJAX call for all entity types
|
||||
$.ajax({
|
||||
url: self.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getTargetEntitiesByIdsBulk',
|
||||
trait: 'EntitySelector',
|
||||
entities: JSON.stringify(bulkRequest)
|
||||
},
|
||||
success: function(response) {
|
||||
if (!response.success || !response.entities) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Process each entity type's results
|
||||
Object.keys(entitiesToLoad).forEach(function(entityType) {
|
||||
var data = entitiesToLoad[entityType];
|
||||
var entities = response.entities[entityType] || [];
|
||||
|
||||
// Build a map of id -> entity for quick lookup
|
||||
var entityMap = {};
|
||||
entities.forEach(function(entity) {
|
||||
entityMap[entity.id] = entity;
|
||||
});
|
||||
|
||||
// Update each picker that requested this entity type
|
||||
data.pickers.forEach(function(pickerData) {
|
||||
var $picker = pickerData.$picker;
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
|
||||
var validIds = [];
|
||||
|
||||
// Replace loading chips with real data
|
||||
pickerData.ids.forEach(function(id) {
|
||||
var $loadingChip = $chips.find('.entity-chip-loading[data-id="' + id + '"]');
|
||||
if (entityMap[id]) {
|
||||
var entity = entityMap[id];
|
||||
validIds.push(entity.id);
|
||||
|
||||
// Create real chip
|
||||
var html = '<span class="entity-chip" data-id="' + self.escapeAttr(entity.id) + '">';
|
||||
if (entity.image) {
|
||||
html += '<span class="chip-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
|
||||
}
|
||||
html += '<span class="chip-name">' + self.escapeHtml(entity.name) + '</span>';
|
||||
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
|
||||
html += '</span>';
|
||||
|
||||
$loadingChip.replaceWith(html);
|
||||
} else {
|
||||
// Entity not found, remove loading chip
|
||||
$loadingChip.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Update chips visibility
|
||||
self.updateChipsVisibility($chips);
|
||||
|
||||
// If some entities were not found, update the hidden input
|
||||
if (validIds.length !== pickerData.ids.length) {
|
||||
$dataInput.val(JSON.stringify(validIds));
|
||||
self.serializeAllBlocks();
|
||||
}
|
||||
|
||||
self.updateBlockStatus($picker.closest('.target-block'));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1021,6 +1182,53 @@
|
||||
this.$wrapper.find('.target-block.active .selection-group').each(function() {
|
||||
self.updateGroupCounts($(this));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch category names by IDs and add chips to the picker
|
||||
* Used when adding selections from the tree modal
|
||||
* @param {jQuery} $picker - Picker element
|
||||
* @param {Array} ids - Category IDs to add
|
||||
* @param {string} entityType - 'categories' or 'cms_categories'
|
||||
* @param {Function} callback - Called when done
|
||||
*/
|
||||
fetchCategoryNamesAndAddChips: function($picker, ids, entityType, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!ids || ids.length === 0) {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getTargetEntitiesByIds',
|
||||
trait: 'EntitySelector',
|
||||
entity_type: entityType,
|
||||
ids: JSON.stringify(ids)
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.entities) {
|
||||
response.entities.forEach(function(entity) {
|
||||
self.addSelectionNoUpdate($picker, entity.id, entity.name, entity);
|
||||
});
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -204,6 +204,11 @@
|
||||
$.extend(instance, mixins.preview);
|
||||
}
|
||||
|
||||
// Merge tree mixin
|
||||
if (mixins.tree) {
|
||||
$.extend(instance, mixins.tree);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
html += '<i class="icon-sort-alpha-asc"></i>';
|
||||
html += '</button>';
|
||||
|
||||
// View mode selector
|
||||
// View mode selector - Tree option always present, shown for categories
|
||||
html += '<select class="view-mode-select" title="View mode">';
|
||||
html += '<option value="list">' + (trans.view_list || 'List') + '</option>';
|
||||
html += '<option value="tree" class="tree-view-option" disabled hidden>' + (trans.view_tree || 'Tree') + '</option>';
|
||||
html += '<option value="tree" class="tree-view-option">' + (trans.view_tree || 'Tree') + '</option>';
|
||||
html += '<option value="cols-2">2 ' + (trans.cols || 'cols') + '</option>';
|
||||
html += '<option value="cols-3">3 ' + (trans.cols || 'cols') + '</option>';
|
||||
html += '<option value="cols-4">4 ' + (trans.cols || 'cols') + '</option>';
|
||||
@@ -347,10 +347,8 @@
|
||||
html += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">0</span> ' + (trans.remaining || 'remaining') + '</span>';
|
||||
html += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
|
||||
html += '</div>';
|
||||
html += '<div class="dropdown-footer-actions">';
|
||||
html += '<button type="button" class="btn-cancel-dropdown">' + (trans.cancel || 'Cancel') + ' <kbd>Esc</kbd></button>';
|
||||
html += '<button type="button" class="btn-confirm-dropdown"><i class="icon-check"></i> ' + (trans.done || 'Done') + ' <kbd>⏎</kbd></button>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="btn-cancel-dropdown"><i class="icon-times"></i> ' + (trans.cancel || 'Cancel') + ' <kbd>Esc</kbd></button>';
|
||||
html += '<button type="button" class="btn-confirm-dropdown"><i class="icon-check"></i> ' + (trans.save || 'Save') + ' <kbd>⏎</kbd></button>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
@@ -403,6 +401,9 @@
|
||||
maxHeight: maxHeight,
|
||||
zIndex: 10000
|
||||
});
|
||||
|
||||
// Show the dropdown
|
||||
this.$dropdown.addClass('show');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -45,11 +45,6 @@
|
||||
this.$wrapper.on('click', '.target-block-tab .tab-badge', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log('[EntitySelector] Tab badge clicked', {
|
||||
hasLoading: $(this).hasClass('loading'),
|
||||
hasPopoverOpen: $(this).hasClass('popover-open'),
|
||||
previewData: $(this).closest('.target-block-tab').data('previewData')
|
||||
});
|
||||
|
||||
var $tab = $(this).closest('.target-block-tab');
|
||||
var $badge = $(this);
|
||||
@@ -65,7 +60,6 @@
|
||||
this.$wrapper.on('click', '.condition-match-count.clickable', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log('[EntitySelector] Condition match count clicked', this);
|
||||
|
||||
var $badge = $(this);
|
||||
|
||||
@@ -80,7 +74,6 @@
|
||||
this.$wrapper.on('click', '.group-count-badge.clickable', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log('[EntitySelector] Group count badge clicked', this);
|
||||
|
||||
var $badge = $(this);
|
||||
|
||||
@@ -91,6 +84,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Total count badge click for summary popover
|
||||
this.$wrapper.on('click', '.trait-total-count', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var $badge = $(this);
|
||||
|
||||
if ($badge.hasClass('popover-open')) {
|
||||
self.hidePreviewPopover();
|
||||
} else {
|
||||
self.showTotalPreviewPopover($badge);
|
||||
}
|
||||
});
|
||||
|
||||
// Close popover when clicking outside
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('.target-preview-popover').length &&
|
||||
@@ -99,7 +106,8 @@
|
||||
!$(e.target).closest('.group-count-badge').length &&
|
||||
!$(e.target).closest('.group-modifiers').length &&
|
||||
!$(e.target).closest('.group-preview-badge').length &&
|
||||
!$(e.target).closest('.toggle-count.clickable').length) {
|
||||
!$(e.target).closest('.toggle-count.clickable').length &&
|
||||
!$(e.target).closest('.trait-total-count').length) {
|
||||
self.hidePreviewPopover();
|
||||
}
|
||||
});
|
||||
@@ -108,7 +116,8 @@
|
||||
this.$wrapper.on('click', '.condition-trait-header', function(e) {
|
||||
if ($(e.target).closest('.target-block-tabs').length ||
|
||||
$(e.target).closest('.trait-header-actions').length ||
|
||||
$(e.target).closest('.prestashop-switch').length) {
|
||||
$(e.target).closest('.prestashop-switch').length ||
|
||||
$(e.target).closest('.trait-total-count').length) {
|
||||
return;
|
||||
}
|
||||
var $body = self.$wrapper.find('.condition-trait-body');
|
||||
@@ -881,6 +890,19 @@
|
||||
searchEntity: searchEntity
|
||||
};
|
||||
|
||||
// Initialize pending selections from current chips
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
self.pendingSelections = [];
|
||||
$chips.find('.entity-chip').each(function() {
|
||||
self.pendingSelections.push({
|
||||
id: $(this).data('id'),
|
||||
name: $(this).find('.chip-name').text(),
|
||||
data: $(this).data()
|
||||
});
|
||||
});
|
||||
self.pendingPicker = $picker;
|
||||
self.pendingRow = section === 'include' ? $group.find('.group-include') : $group.find('.exclude-row[data-exclude-index="' + excludeIndex + '"]');
|
||||
|
||||
self.searchOffset = 0;
|
||||
self.searchQuery = $(this).val().trim();
|
||||
|
||||
@@ -892,7 +914,9 @@
|
||||
|
||||
self.positionDropdown($(this));
|
||||
|
||||
if (self.viewMode === 'tree') {
|
||||
// For tree view mode on categories, load category tree instead of search
|
||||
if (self.viewMode === 'tree' && (searchEntity === 'categories' || searchEntity === 'cms_categories')) {
|
||||
self.loadCategoryTree();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -955,6 +979,10 @@
|
||||
// Dropdown item click
|
||||
this.$dropdown.on('click', '.dropdown-item', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Blur any focused input so Ctrl+A works for select all
|
||||
$(document.activeElement).filter('input, textarea').blur();
|
||||
|
||||
var $item = $(this);
|
||||
var id = $item.data('id');
|
||||
var name = $item.data('name');
|
||||
@@ -1032,6 +1060,38 @@
|
||||
e.preventDefault();
|
||||
if (!self.activeGroup) return;
|
||||
|
||||
// Handle tree view - use pending selections
|
||||
if (self.viewMode === 'tree') {
|
||||
if (!self.pendingSelections) self.pendingSelections = [];
|
||||
|
||||
// Select all visible (not filtered-out) tree items
|
||||
var $visibleTreeItems = self.$dropdown.find('.tree-item:not(.filtered-out)');
|
||||
$visibleTreeItems.each(function() {
|
||||
var $item = $(this);
|
||||
var id = parseInt($item.data('id'), 10);
|
||||
var name = $item.data('name');
|
||||
|
||||
if (!$item.hasClass('selected')) {
|
||||
$item.addClass('selected');
|
||||
var exists = self.pendingSelections.some(function(s) {
|
||||
return parseInt(s.id, 10) === id;
|
||||
});
|
||||
if (!exists) {
|
||||
self.pendingSelections.push({ id: id, name: name, data: $item.data() });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update count display
|
||||
var selectedCount = self.$dropdown.find('.tree-item.selected').length;
|
||||
var totalCount = self.$dropdown.find('.tree-item').length;
|
||||
var entityType = self.$dropdown.find('.category-tree').data('entity-type') || 'categories';
|
||||
var categoryLabel = entityType === 'cms_categories' ? 'CMS categories' : 'categories';
|
||||
self.$dropdown.find('.results-count').text(totalCount + ' ' + categoryLabel + ' (' + selectedCount + ' selected)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle list view
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
@@ -1067,6 +1127,20 @@
|
||||
e.preventDefault();
|
||||
if (!self.activeGroup) return;
|
||||
|
||||
// Handle tree view - clear pending selections
|
||||
if (self.viewMode === 'tree') {
|
||||
self.pendingSelections = [];
|
||||
self.$dropdown.find('.tree-item').removeClass('selected');
|
||||
|
||||
// Update count display
|
||||
var totalCount = self.$dropdown.find('.tree-item').length;
|
||||
var entityType = self.$dropdown.find('.category-tree').data('entity-type') || 'categories';
|
||||
var categoryLabel = entityType === 'cms_categories' ? 'CMS categories' : 'categories';
|
||||
self.$dropdown.find('.results-count').text(totalCount + ' ' + categoryLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle list view
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
@@ -1087,15 +1161,43 @@
|
||||
self.serializeAllBlocks($row);
|
||||
});
|
||||
|
||||
// Done/confirm
|
||||
// Save - commit pending selections to chips
|
||||
this.$dropdown.on('click', '.btn-confirm-dropdown', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (self.pendingPicker && self.pendingSelections) {
|
||||
var $chips = self.pendingPicker.find('.entity-chips');
|
||||
|
||||
// Clear existing chips
|
||||
$chips.empty();
|
||||
|
||||
// Add chips for all pending selections
|
||||
self.pendingSelections.forEach(function(sel) {
|
||||
self.addSelectionNoUpdate(self.pendingPicker, sel.id, sel.name, sel.data);
|
||||
});
|
||||
|
||||
self.updateChipsVisibility($chips);
|
||||
|
||||
// Serialize to hidden input
|
||||
if (self.pendingRow) {
|
||||
self.serializeAllBlocks(self.pendingRow);
|
||||
}
|
||||
}
|
||||
|
||||
self.pendingSelections = null;
|
||||
self.pendingPicker = null;
|
||||
self.pendingRow = null;
|
||||
self.hideDropdown();
|
||||
});
|
||||
|
||||
// Cancel
|
||||
// Cancel - discard pending selections (no changes to chips)
|
||||
this.$dropdown.on('click', '.btn-cancel-dropdown', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Just discard pending - chips remain unchanged
|
||||
self.pendingSelections = null;
|
||||
self.pendingPicker = null;
|
||||
self.pendingRow = null;
|
||||
self.hideDropdown();
|
||||
});
|
||||
|
||||
@@ -1128,21 +1230,6 @@
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
// View mode change
|
||||
this.$dropdown.on('change', '.view-mode-select', function() {
|
||||
var value = $(this).val();
|
||||
self.viewMode = value;
|
||||
self.$dropdown.removeClass('view-list view-tree view-cols-2 view-cols-3 view-cols-4 view-cols-5 view-cols-6 view-cols-7 view-cols-8');
|
||||
self.$dropdown.addClass('view-' + value);
|
||||
|
||||
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : '';
|
||||
if (value === 'tree' && (searchEntity === 'categories' || searchEntity === 'cms_categories')) {
|
||||
self.loadCategoryTree();
|
||||
} else if (value !== 'tree') {
|
||||
self.performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Tree view: Toggle expand/collapse
|
||||
this.$dropdown.on('click', '.category-tree .tree-toggle', function(e) {
|
||||
e.stopPropagation();
|
||||
@@ -1161,32 +1248,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Tree view: Item click (select/deselect)
|
||||
// Tree view: Item click (select/deselect) - PENDING mode
|
||||
this.$dropdown.on('click', '.category-tree .tree-item', function(e) {
|
||||
if ($(e.target).closest('.tree-toggle, .btn-select-children').length) {
|
||||
if ($(e.target).closest('.tree-toggle, .btn-select-children, .tree-count').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Blur any focused input so Ctrl+A works for select all
|
||||
$(document.activeElement).filter('input, textarea').blur();
|
||||
|
||||
var $item = $(this);
|
||||
var id = $item.data('id');
|
||||
var id = parseInt($item.data('id'), 10);
|
||||
var name = $item.data('name');
|
||||
var isSelected = $item.hasClass('selected');
|
||||
|
||||
if (!self.activeGroup) return;
|
||||
|
||||
var $block = self.$wrapper.find('.target-block[data-block-type="' + self.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + self.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
var $row;
|
||||
|
||||
if (self.activeGroup.section === 'include') {
|
||||
$picker = $group.find('.include-picker');
|
||||
$row = $group.find('.group-include');
|
||||
} else {
|
||||
var $excludeRow = $group.find('.exclude-row[data-exclude-index="' + self.activeGroup.excludeIndex + '"]');
|
||||
$picker = $excludeRow.find('.exclude-picker');
|
||||
$row = $excludeRow;
|
||||
}
|
||||
if (!self.pendingSelections) self.pendingSelections = [];
|
||||
|
||||
var $allItems = self.$dropdown.find('.tree-item');
|
||||
|
||||
@@ -1200,27 +1277,43 @@
|
||||
};
|
||||
|
||||
if (isSelected) {
|
||||
self.removeSelection($picker, id);
|
||||
$item.toggleClass('selected');
|
||||
self.serializeAllBlocks($row);
|
||||
updateCount();
|
||||
// Remove from pending
|
||||
self.pendingSelections = self.pendingSelections.filter(function(s) {
|
||||
return parseInt(s.id, 10) !== id;
|
||||
});
|
||||
$item.removeClass('selected');
|
||||
} else {
|
||||
var currentSelection = self.getCurrentSingleSelection();
|
||||
if (currentSelection) {
|
||||
var newEntityType = self.activeGroup.blockType;
|
||||
self.showReplaceConfirmation(currentSelection, { name: name, entityType: newEntityType }, function() {
|
||||
self.$dropdown.find('.tree-item.selected').removeClass('selected');
|
||||
self.addSelection($picker, id, name, $item.data());
|
||||
$item.addClass('selected');
|
||||
self.serializeAllBlocks($row);
|
||||
updateCount();
|
||||
// Add to pending
|
||||
var exists = self.pendingSelections.some(function(s) {
|
||||
return parseInt(s.id, 10) === id;
|
||||
});
|
||||
if (!exists) {
|
||||
self.pendingSelections.push({
|
||||
id: id,
|
||||
name: name,
|
||||
data: $item.data()
|
||||
});
|
||||
} else {
|
||||
self.addSelection($picker, id, name, $item.data());
|
||||
$item.toggleClass('selected');
|
||||
self.serializeAllBlocks($row);
|
||||
updateCount();
|
||||
}
|
||||
$item.addClass('selected');
|
||||
}
|
||||
|
||||
updateCount();
|
||||
});
|
||||
|
||||
// Tree view: Product/page count click - show preview
|
||||
this.$dropdown.on('click', '.category-tree .tree-count.clickable', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var $count = $(this);
|
||||
var categoryId = $count.data('category-id');
|
||||
var $item = $count.closest('.tree-item');
|
||||
var categoryName = $item.data('name');
|
||||
var entityType = self.$dropdown.find('.category-tree').data('entity-type') || 'categories';
|
||||
|
||||
if ($count.hasClass('popover-open')) {
|
||||
self.hidePreviewPopover();
|
||||
} else {
|
||||
self.showCategoryItemsPreview($count, categoryId, categoryName, entityType);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1342,6 +1435,11 @@
|
||||
|
||||
clearTimeout(self.refineTimeout);
|
||||
self.refineTimeout = setTimeout(function() {
|
||||
// For tree view, filter client-side instead of server refresh
|
||||
if (self.viewMode === 'tree') {
|
||||
self.filterCategoryTree(query);
|
||||
return;
|
||||
}
|
||||
self.refreshSearch();
|
||||
}, 300);
|
||||
});
|
||||
@@ -1352,6 +1450,11 @@
|
||||
self.refineQuery = '';
|
||||
self.$dropdown.find('.refine-input').val('');
|
||||
$(this).hide();
|
||||
// For tree view, filter client-side instead of server refresh
|
||||
if (self.viewMode === 'tree') {
|
||||
self.filterCategoryTree('');
|
||||
return;
|
||||
}
|
||||
self.refreshSearch();
|
||||
});
|
||||
|
||||
@@ -1589,6 +1692,7 @@
|
||||
// View mode select change
|
||||
this.$dropdown.on('change', '.view-mode-select', function() {
|
||||
var mode = $(this).val();
|
||||
var prevMode = self.viewMode;
|
||||
self.viewMode = mode;
|
||||
|
||||
// Remove all view mode classes and add the new one
|
||||
@@ -1596,12 +1700,18 @@
|
||||
.removeClass('view-list 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-' + mode.replace('cols-', 'cols-'));
|
||||
|
||||
// For tree view, load the category tree
|
||||
if (mode === 'tree') {
|
||||
// For tree view, load the category tree (only for categories/cms_categories)
|
||||
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : '';
|
||||
if (mode === 'tree' && (searchEntity === 'categories' || searchEntity === 'cms_categories')) {
|
||||
self.loadCategoryTree();
|
||||
} else {
|
||||
// Re-render current results with new view mode
|
||||
self.renderSearchResults(false);
|
||||
} else if (mode !== 'tree') {
|
||||
// If switching FROM tree mode, need to refresh search to load data
|
||||
if (prevMode === 'tree') {
|
||||
self.refreshSearch();
|
||||
} else {
|
||||
// Re-render current results with new view mode
|
||||
self.renderSearchResults(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1654,16 +1764,21 @@
|
||||
$(document).on('keydown', function(e) {
|
||||
if (!self.$dropdown || !self.$dropdown.hasClass('show')) return;
|
||||
|
||||
// Ctrl+A / Cmd+A - Select All
|
||||
// Allow default behavior in input/textarea fields
|
||||
var isInputFocused = $(document.activeElement).is('input, textarea');
|
||||
|
||||
// Ctrl+A / Cmd+A - Select All (only when not in input)
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 65) {
|
||||
if (isInputFocused) return; // Let browser select text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.$dropdown.find('.btn-select-all').trigger('click');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ctrl+D / Cmd+D - Clear/Deselect all
|
||||
// Ctrl+D / Cmd+D - Clear/Deselect all (only when not in input)
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 68) {
|
||||
if (isInputFocused) return; // Let browser handle
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.$dropdown.find('.btn-clear-selection').trigger('click');
|
||||
|
||||
@@ -104,7 +104,9 @@
|
||||
},
|
||||
|
||||
updateFilterPanelForEntity: function(entityType) {
|
||||
if (!this.$dropdown) return;
|
||||
if (!this.$dropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $panel = this.$dropdown.find('.filter-panel');
|
||||
|
||||
@@ -115,12 +117,20 @@
|
||||
$panel.find('.filter-row[data-entity="' + entityType + '"]').show();
|
||||
$panel.find('.filter-row-entity-' + entityType.replace('_', '-')).show();
|
||||
|
||||
// Enable/disable tree view option
|
||||
var $treeOption = this.$dropdown.find('.tree-view-option');
|
||||
if (entityType === 'categories' || entityType === 'cms_categories') {
|
||||
$treeOption.prop('disabled', false).show();
|
||||
} else {
|
||||
$treeOption.prop('disabled', true).hide();
|
||||
// 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');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -200,6 +200,8 @@
|
||||
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');
|
||||
@@ -216,9 +218,7 @@
|
||||
$tab.append('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
|
||||
}
|
||||
$tab.addClass('has-data');
|
||||
|
||||
// Fetch real product count
|
||||
self.fetchProductCount(blockType, $tab);
|
||||
blockTypesWithData.push(blockType);
|
||||
} else {
|
||||
$badge.remove();
|
||||
$tab.removeClass('has-data');
|
||||
@@ -227,6 +227,11 @@
|
||||
|
||||
// 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() {
|
||||
@@ -252,6 +257,100 @@
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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('<i class="icon-eye"></i> ' + 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 = {};
|
||||
@@ -310,10 +409,12 @@
|
||||
// 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() {
|
||||
error: function(xhr, status, error) {
|
||||
console.error('[EntitySelector] AJAX error for', blockType, ':', status, error);
|
||||
$tab.find('.tab-badge').remove();
|
||||
self.updateHeaderTotalCount();
|
||||
}
|
||||
@@ -337,7 +438,8 @@
|
||||
|
||||
var $totalBadge = this.$wrapper.find('.trait-total-count');
|
||||
if (total > 0) {
|
||||
$totalBadge.text(total).show();
|
||||
$totalBadge.find('.count-value').text(total);
|
||||
$totalBadge.show();
|
||||
} else {
|
||||
$totalBadge.hide();
|
||||
}
|
||||
@@ -710,40 +812,98 @@
|
||||
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() {
|
||||
self.updateGroupCounts($(this));
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateGroupCounts: function($group) {
|
||||
var self = this;
|
||||
|
||||
// Update include count
|
||||
var $include = $group.find('.group-include');
|
||||
if ($include.length) {
|
||||
this.updateConditionCount($include);
|
||||
// If no conditions, nothing to do
|
||||
if (Object.keys(conditions).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update each exclude row count
|
||||
$group.find('.exclude-row').each(function() {
|
||||
self.updateConditionCount($(this));
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update group total count (include - excludes)
|
||||
this.updateGroupTotalCount($group);
|
||||
},
|
||||
|
||||
updateConditionCount: function($row) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
// Find the count element - in method-selector-wrapper for include, in exclude-header-row for exclude
|
||||
/**
|
||||
* 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;
|
||||
if (!$countEl.length) return null;
|
||||
|
||||
// Determine if this is an include or exclude row
|
||||
var isExclude = $row.hasClass('exclude-row');
|
||||
var $methodSelect = isExclude
|
||||
? $row.find('.exclude-method-select')
|
||||
@@ -752,10 +912,9 @@
|
||||
var method = $methodSelect.val();
|
||||
if (!method) {
|
||||
$countEl.hide();
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the picker and extract values
|
||||
var $picker = isExclude
|
||||
? $row.find('.exclude-picker')
|
||||
: $row.find('.include-picker');
|
||||
@@ -767,9 +926,87 @@
|
||||
var hasNoValues = !values ||
|
||||
(Array.isArray(values) && values.length === 0) ||
|
||||
(typeof values === 'object' && !Array.isArray(values) && (
|
||||
// For combination_attributes, check if attributes object is empty
|
||||
(valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
|
||||
// For other objects, check if completely empty
|
||||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
|
||||
));
|
||||
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) {
|
||||
$countEl.hide();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show loading spinner
|
||||
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
|
||||
$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) 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) {
|
||||
$countEl.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var $picker = isExclude
|
||||
? $row.find('.exclude-picker')
|
||||
: $row.find('.include-picker');
|
||||
|
||||
var valueType = $picker.data('valueType') || 'none';
|
||||
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 !== 'none' && valueType !== 'boolean' && hasNoValues) {
|
||||
@@ -777,15 +1014,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Get block type
|
||||
var $block = $row.closest('.target-block');
|
||||
var blockType = $block.data('blockType') || 'products';
|
||||
if (!blockType) {
|
||||
var $block = $row.closest('.target-block');
|
||||
blockType = $block.data('blockType') || 'products';
|
||||
}
|
||||
|
||||
// Show loading
|
||||
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
|
||||
$countEl.removeClass('clickable no-matches').show();
|
||||
|
||||
// Store condition data on badge for popover
|
||||
$countEl.data('conditionData', {
|
||||
method: method,
|
||||
values: values,
|
||||
@@ -813,7 +1049,6 @@
|
||||
$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();
|
||||
}
|
||||
@@ -917,6 +1152,17 @@
|
||||
html += '</button>';
|
||||
|
||||
$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();
|
||||
},
|
||||
@@ -937,7 +1183,13 @@
|
||||
|
||||
// Enhance the exclude method select with styled dropdown
|
||||
var $newRow = $container.find('.exclude-row[data-exclude-index="' + excludeIndex + '"]');
|
||||
this.enhanceMethodSelect($newRow.find('.exclude-method-select'));
|
||||
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();
|
||||
},
|
||||
|
||||
@@ -844,7 +844,7 @@
|
||||
}
|
||||
|
||||
$wrapper.addClass('selector-locked');
|
||||
if (!$wrapper.find('.mpr-info-wrapper').length) {
|
||||
if (!$wrapper.find('.lock-indicator').length) {
|
||||
var lockHtml = '<span class="mpr-info-wrapper lock-indicator">' +
|
||||
'<i class="icon-lock"></i>' +
|
||||
'<span class="mpr-tooltip">' +
|
||||
|
||||
@@ -1091,6 +1091,145 @@
|
||||
});
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// CATEGORY ITEMS PREVIEW (products/pages in a category)
|
||||
// =========================================================================
|
||||
|
||||
showCategoryItemsPreview: function($badge, categoryId, categoryName, entityType) {
|
||||
var self = this;
|
||||
|
||||
this.hidePreviewPopover();
|
||||
|
||||
$badge.addClass('popover-open loading');
|
||||
this.$activeBadge = $badge;
|
||||
|
||||
var isProducts = (entityType === 'categories');
|
||||
var entityLabelPlural = isProducts ? 'products' : 'pages';
|
||||
var action = isProducts ? 'previewCategoryProducts' : 'previewCategoryPages';
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: action,
|
||||
trait: 'EntitySelector',
|
||||
category_id: categoryId,
|
||||
limit: 10
|
||||
},
|
||||
success: function(response) {
|
||||
$badge.removeClass('loading');
|
||||
|
||||
if (response.success) {
|
||||
self.createPreviewPopover({
|
||||
$badge: $badge,
|
||||
items: response.items || [],
|
||||
totalCount: response.count || 0,
|
||||
hasMore: response.hasMore || false,
|
||||
entityLabel: entityLabelPlural,
|
||||
previewType: 'category-items',
|
||||
context: { categoryId: categoryId, categoryName: categoryName, entityType: entityType },
|
||||
onLoadMore: function($btn) {
|
||||
self.loadMoreCategoryItems($btn);
|
||||
},
|
||||
onFilter: function(query) {
|
||||
self.filterCategoryItems(query);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$badge.removeClass('popover-open');
|
||||
self.$activeBadge = null;
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$badge.removeClass('loading popover-open');
|
||||
self.$activeBadge = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreCategoryItems: function($btn) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.categoryId) return;
|
||||
|
||||
var isProducts = (ctx.entityType === 'categories');
|
||||
var action = isProducts ? 'previewCategoryProducts' : 'previewCategoryPages';
|
||||
|
||||
$btn.prop('disabled', true).find('i').addClass('icon-spin');
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: action,
|
||||
trait: 'EntitySelector',
|
||||
category_id: ctx.categoryId,
|
||||
offset: this.previewOffset,
|
||||
limit: 10,
|
||||
query: this.previewFilterQuery || ''
|
||||
},
|
||||
success: function(response) {
|
||||
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
|
||||
|
||||
if (response.success && response.items) {
|
||||
self.appendPreviewItems(response.items);
|
||||
self.previewOffset += response.items.length;
|
||||
|
||||
if (!response.hasMore) {
|
||||
$btn.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$btn.prop('disabled', false).find('i').removeClass('icon-spin');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
filterCategoryItems: function(query) {
|
||||
var self = this;
|
||||
var ctx = this.previewContext;
|
||||
|
||||
if (!ctx || !ctx.categoryId) {
|
||||
self.showFilterLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var isProducts = (ctx.entityType === 'categories');
|
||||
var action = isProducts ? 'previewCategoryProducts' : 'previewCategoryPages';
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: action,
|
||||
trait: 'EntitySelector',
|
||||
category_id: ctx.categoryId,
|
||||
query: query,
|
||||
limit: 10
|
||||
},
|
||||
success: function(response) {
|
||||
self.showFilterLoading(false);
|
||||
|
||||
if (response.success) {
|
||||
self.replacePreviewItems(response.items || [], response.count || 0, response.hasMore || false);
|
||||
self.previewOffset = response.items ? response.items.length : 0;
|
||||
self.previewFilterQuery = query;
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showFilterLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// PATTERN PREVIEW MODAL (for regex/pattern matching)
|
||||
// =========================================================================
|
||||
@@ -1214,6 +1353,110 @@
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// TOTAL COUNT PREVIEW (Header total badge click)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Show preview popover for total count badge
|
||||
* Displays a summary of all entity types with their counts
|
||||
*/
|
||||
showTotalPreviewPopover: function($badge) {
|
||||
console.log('[EntitySelector] showTotalPreviewPopover called', { badge: $badge[0] });
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
this.hidePreviewPopover();
|
||||
|
||||
$badge.addClass('popover-open');
|
||||
this.$activeBadge = $badge;
|
||||
|
||||
// Collect all entity types with data
|
||||
var summaryItems = [];
|
||||
console.log('[EntitySelector] Looking for tabs with data...');
|
||||
this.$wrapper.find('.target-block-tab.has-data').each(function() {
|
||||
var $tab = $(this);
|
||||
var blockType = $tab.data('blockType');
|
||||
var $tabBadge = $tab.find('.tab-badge');
|
||||
var countText = $tabBadge.text().replace(/[^0-9]/g, '');
|
||||
var count = parseInt(countText, 10) || 0;
|
||||
|
||||
if (count > 0) {
|
||||
var blockConfig = self.config.blocks && self.config.blocks[blockType] ? self.config.blocks[blockType] : {};
|
||||
var icon = $tab.find('.tab-label').prev('i').attr('class') || 'icon-cube';
|
||||
var label = $tab.find('.tab-label').text() || blockType;
|
||||
|
||||
summaryItems.push({
|
||||
blockType: blockType,
|
||||
label: label,
|
||||
icon: icon,
|
||||
count: count
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[EntitySelector] Summary items collected:', summaryItems);
|
||||
|
||||
// Build popover HTML
|
||||
var totalCount = parseInt($badge.find('.count-value').text(), 10) || 0;
|
||||
console.log('[EntitySelector] Building popover, totalCount:', totalCount);
|
||||
var popoverHtml = '<div class="target-preview-popover total-preview-popover">';
|
||||
popoverHtml += '<div class="preview-popover-header">';
|
||||
popoverHtml += '<span class="preview-popover-title">' + (trans.total_summary || 'Selection Summary') + '</span>';
|
||||
popoverHtml += '<span class="preview-popover-count">' + totalCount + ' ' + (trans.total_items || 'total items') + '</span>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '<div class="preview-popover-body">';
|
||||
popoverHtml += '<ul class="total-summary-list">';
|
||||
|
||||
for (var i = 0; i < summaryItems.length; i++) {
|
||||
var item = summaryItems[i];
|
||||
popoverHtml += '<li class="total-summary-item" data-block-type="' + item.blockType + '">';
|
||||
popoverHtml += '<i class="' + self.escapeAttr(item.icon) + '"></i>';
|
||||
popoverHtml += '<span class="summary-item-label">' + self.escapeHtml(item.label) + '</span>';
|
||||
popoverHtml += '<span class="summary-item-count">' + item.count + '</span>';
|
||||
popoverHtml += '</li>';
|
||||
}
|
||||
|
||||
popoverHtml += '</ul>';
|
||||
popoverHtml += '</div>';
|
||||
popoverHtml += '</div>';
|
||||
|
||||
var $popover = $(popoverHtml);
|
||||
this.$previewPopover = $popover;
|
||||
|
||||
// Click on item to switch to that tab
|
||||
$popover.on('click', '.total-summary-item', function() {
|
||||
var blockType = $(this).data('blockType');
|
||||
self.hidePreviewPopover();
|
||||
self.switchToBlock(blockType);
|
||||
});
|
||||
|
||||
// Position popover
|
||||
$('body').append($popover);
|
||||
var badgeOffset = $badge.offset();
|
||||
var badgeHeight = $badge.outerHeight();
|
||||
var popoverWidth = $popover.outerWidth();
|
||||
|
||||
$popover.css({
|
||||
position: 'absolute',
|
||||
top: badgeOffset.top + badgeHeight + 5,
|
||||
left: badgeOffset.left - (popoverWidth / 2) + ($badge.outerWidth() / 2),
|
||||
zIndex: 10000
|
||||
});
|
||||
|
||||
// Adjust if off screen
|
||||
var windowWidth = $(window).width();
|
||||
var popoverRight = $popover.offset().left + popoverWidth;
|
||||
if (popoverRight > windowWidth - 10) {
|
||||
$popover.css('left', windowWidth - popoverWidth - 10);
|
||||
}
|
||||
if ($popover.offset().left < 10) {
|
||||
$popover.css('left', 10);
|
||||
}
|
||||
|
||||
$popover.hide().fadeIn(150);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -475,331 +475,9 @@
|
||||
this.$dropdown.find('.btn-show-history').prop('disabled', !hasHistory);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load and display category tree view
|
||||
*/
|
||||
loadCategoryTree: function() {
|
||||
var self = this;
|
||||
var $container = this.$dropdown.find('.dropdown-results');
|
||||
var entityType = this.activeGroup ? this.activeGroup.searchEntity : 'categories';
|
||||
|
||||
// Show the dropdown
|
||||
this.$dropdown.addClass('show');
|
||||
|
||||
// Show loading state
|
||||
$container.html('<div class="tree-loading"><i class="icon-spinner icon-spin"></i> Loading category tree...</div>');
|
||||
|
||||
// Use separate cache for each entity type
|
||||
var cacheKey = entityType + 'TreeCache';
|
||||
if (this[cacheKey]) {
|
||||
this.renderCategoryTree(this[cacheKey], entityType);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getCategoryTree',
|
||||
trait: 'EntitySelector',
|
||||
entity_type: entityType
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.categories) {
|
||||
self[cacheKey] = response.categories;
|
||||
self.renderCategoryTree(response.categories, entityType);
|
||||
} else {
|
||||
$container.html('<div class="no-results"><i class="icon-warning"></i> Failed to load category tree</div>');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$container.html('<div class="no-results"><i class="icon-warning"></i> Error loading category tree</div>');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Render category tree structure
|
||||
*/
|
||||
renderCategoryTree: function(categories, entityType) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
var $container = this.$dropdown.find('.dropdown-results');
|
||||
var isCmsCategory = entityType === 'cms_categories';
|
||||
var categoryLabel = isCmsCategory ? 'CMS categories' : 'categories';
|
||||
|
||||
// Get selected IDs from current picker
|
||||
var selectedIds = [];
|
||||
if (this.activeGroup) {
|
||||
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
|
||||
|
||||
if (this.activeGroup.section === 'include') {
|
||||
var $picker = $group.find('.include-picker');
|
||||
$picker.find('.entity-chip').each(function() {
|
||||
selectedIds.push(parseInt($(this).data('id'), 10));
|
||||
});
|
||||
} else {
|
||||
var $currentExcludeRow = $group.find('.exclude-row[data-exclude-index="' + this.activeGroup.excludeIndex + '"]');
|
||||
var $currentPicker = $currentExcludeRow.find('.exclude-picker');
|
||||
$currentPicker.find('.entity-chip').each(function() {
|
||||
selectedIds.push(parseInt($(this).data('id'), 10));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build tree HTML
|
||||
var html = '<div class="category-tree" data-entity-type="' + this.escapeAttr(entityType) + '">';
|
||||
|
||||
html += '<div class="tree-container">';
|
||||
|
||||
// Find minimum level (usually 1 or 2)
|
||||
var minLevel = categories.length > 0 ? categories[0].level : 1;
|
||||
|
||||
categories.forEach(function(cat) {
|
||||
var isSelected = selectedIds.indexOf(cat.id) !== -1;
|
||||
var indent = (cat.level - minLevel) * 20;
|
||||
var hasChildren = cat.has_children;
|
||||
|
||||
html += '<div class="tree-item' + (isSelected ? ' selected' : '') + (hasChildren ? ' has-children' : '') + '" ';
|
||||
html += 'data-id="' + cat.id + '" ';
|
||||
html += 'data-parent-id="' + cat.parent_id + '" ';
|
||||
html += 'data-level="' + cat.level + '" ';
|
||||
html += 'data-nleft="' + cat.nleft + '" ';
|
||||
html += 'data-nright="' + cat.nright + '" ';
|
||||
html += 'data-name="' + self.escapeAttr(cat.name) + '" ';
|
||||
html += 'data-subtitle="' + self.escapeAttr(cat.subtitle) + '" ';
|
||||
html += 'style="padding-left: ' + (indent + 8) + 'px;">';
|
||||
|
||||
// Expand/collapse toggle for parents
|
||||
if (hasChildren) {
|
||||
html += '<span class="tree-toggle"><i class="icon-caret-down"></i></span>';
|
||||
} else {
|
||||
html += '<span class="tree-toggle-placeholder"></span>';
|
||||
}
|
||||
|
||||
// Select children button for parents (on the left, near toggle)
|
||||
// Hide in single mode - selecting multiple items doesn't make sense there
|
||||
var isSingleMode = self.config.mode === 'single';
|
||||
if (hasChildren && !isSingleMode) {
|
||||
html += '<button type="button" class="btn-select-children" title="' + (trans.select_with_children || 'Select with all children') + '">';
|
||||
html += '<i class="icon-plus-square"></i>';
|
||||
html += '</button>';
|
||||
} else if (!isSingleMode) {
|
||||
html += '<span class="btn-select-children-placeholder"></span>';
|
||||
}
|
||||
|
||||
// Checkbox
|
||||
html += '<span class="tree-checkbox"><i class="icon-check"></i></span>';
|
||||
|
||||
// Category icon (file icon for CMS categories)
|
||||
var iconClass = isCmsCategory ? 'icon-file-text-o' : ('icon-folder' + (hasChildren ? '' : '-o'));
|
||||
html += '<span class="tree-icon"><i class="' + iconClass + '"></i></span>';
|
||||
|
||||
// Name and subtitle
|
||||
html += '<div class="tree-info">';
|
||||
html += '<span class="tree-name">' + self.escapeHtml(cat.name) + '</span>';
|
||||
html += '<span class="tree-subtitle">' + self.escapeHtml(cat.subtitle) + '</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>'; // tree-container
|
||||
html += '</div>'; // category-tree
|
||||
|
||||
$container.html(html);
|
||||
|
||||
// Update results count with appropriate label
|
||||
var selectedCount = $container.find('.tree-item.selected').length;
|
||||
this.$dropdown.find('.results-count').text(categories.length + ' ' + categoryLabel + (selectedCount > 0 ? ' (' + selectedCount + ' selected)' : ''));
|
||||
|
||||
// Update select-children button states based on initial selection
|
||||
var $allItems = $container.find('.tree-item');
|
||||
this.updateSelectChildrenButtons($allItems);
|
||||
|
||||
// Hide load more controls in tree view
|
||||
this.$dropdown.find('.load-more-controls').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter category tree by search query (client-side filtering)
|
||||
*/
|
||||
filterCategoryTree: function(query) {
|
||||
var self = this;
|
||||
var $container = this.$dropdown.find('.category-tree');
|
||||
if (!$container.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $items = $container.find('.tree-item');
|
||||
query = query.toLowerCase().trim();
|
||||
|
||||
if (!query) {
|
||||
// Show all items when query is empty
|
||||
$items.show().removeClass('collapsed');
|
||||
$container.find('.tree-toggle i').removeClass('icon-caret-right').addClass('icon-caret-down');
|
||||
return;
|
||||
}
|
||||
|
||||
// First pass: find matching items and their ancestors
|
||||
var matchingIds = [];
|
||||
var ancestorIds = [];
|
||||
|
||||
$items.each(function() {
|
||||
var $item = $(this);
|
||||
var name = ($item.data('name') || '').toLowerCase();
|
||||
if (name.indexOf(query) !== -1) {
|
||||
matchingIds.push($item.data('id'));
|
||||
// Also mark all ancestors using helper (works for both nleft/nright and parent_id)
|
||||
var ancestors = self.findTreeAncestors($item, $items);
|
||||
for (var i = 0; i < ancestors.length; i++) {
|
||||
ancestorIds.push($(ancestors[i]).data('id'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: show/hide items
|
||||
$items.each(function() {
|
||||
var $item = $(this);
|
||||
var id = $item.data('id');
|
||||
if (matchingIds.indexOf(id) !== -1 || ancestorIds.indexOf(id) !== -1) {
|
||||
$item.show().removeClass('collapsed');
|
||||
$item.find('.tree-toggle i').removeClass('icon-caret-right').addClass('icon-caret-down');
|
||||
} else {
|
||||
$item.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Update count with appropriate label
|
||||
var visibleCount = $items.filter(':visible').length;
|
||||
var selectedCount = $items.filter('.selected').length;
|
||||
var entityType = $container.data('entity-type') || 'categories';
|
||||
var categoryLabel = entityType === 'cms_categories' ? 'CMS categories' : 'categories';
|
||||
this.$dropdown.find('.results-count').text(visibleCount + ' ' + categoryLabel + (selectedCount > 0 ? ' (' + selectedCount + ' selected)' : ''));
|
||||
},
|
||||
|
||||
/**
|
||||
* Find all descendant tree items of a parent.
|
||||
* Works with nleft/nright (product categories) or parent_id (CMS categories).
|
||||
*/
|
||||
findTreeDescendants: function($parent, $allItems) {
|
||||
var nleft = parseInt($parent.data('nleft'), 10);
|
||||
var nright = parseInt($parent.data('nright'), 10);
|
||||
var parentId = parseInt($parent.data('id'), 10);
|
||||
var descendants = [];
|
||||
|
||||
// If nleft/nright are valid (product categories), use nested set
|
||||
if (nleft > 0 && nright > 0 && nright > nleft) {
|
||||
$allItems.each(function() {
|
||||
var $item = $(this);
|
||||
var childNleft = parseInt($item.data('nleft'), 10);
|
||||
var childNright = parseInt($item.data('nright'), 10);
|
||||
if (childNleft > nleft && childNright < nright) {
|
||||
descendants.push($item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// CMS categories: use parent_id recursively
|
||||
var idsToCheck = [parentId];
|
||||
var processed = {};
|
||||
|
||||
while (idsToCheck.length > 0) {
|
||||
var checkId = idsToCheck.shift();
|
||||
if (processed[checkId]) continue;
|
||||
processed[checkId] = true;
|
||||
|
||||
$allItems.each(function() {
|
||||
var $item = $(this);
|
||||
var itemParentId = parseInt($item.data('parent-id'), 10);
|
||||
var itemId = parseInt($item.data('id'), 10);
|
||||
if (itemParentId === checkId && !processed[itemId]) {
|
||||
descendants.push($item);
|
||||
idsToCheck.push(itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return descendants;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find all ancestor tree items of an item.
|
||||
* Works with nleft/nright (product categories) or parent_id (CMS categories).
|
||||
*/
|
||||
findTreeAncestors: function($item, $allItems) {
|
||||
var nleft = parseInt($item.data('nleft'), 10);
|
||||
var nright = parseInt($item.data('nright'), 10);
|
||||
var ancestors = [];
|
||||
|
||||
// If nleft/nright are valid (product categories), use nested set
|
||||
if (nleft > 0 && nright > 0) {
|
||||
$allItems.each(function() {
|
||||
var $ancestor = $(this);
|
||||
var ancNleft = parseInt($ancestor.data('nleft'), 10);
|
||||
var ancNright = parseInt($ancestor.data('nright'), 10);
|
||||
if (ancNleft < nleft && ancNright > nright) {
|
||||
ancestors.push($ancestor);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// CMS categories: use parent_id chain
|
||||
var parentId = parseInt($item.data('parent-id'), 10);
|
||||
var processed = {};
|
||||
|
||||
while (parentId > 0 && !processed[parentId]) {
|
||||
processed[parentId] = true;
|
||||
$allItems.each(function() {
|
||||
var $ancestor = $(this);
|
||||
var ancestorId = parseInt($ancestor.data('id'), 10);
|
||||
if (ancestorId === parentId) {
|
||||
ancestors.push($ancestor);
|
||||
parentId = parseInt($ancestor.data('parent-id'), 10);
|
||||
return false; // break inner loop
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update all select-children buttons to reflect current selection state.
|
||||
* Shows minus icon if item and all children are selected, plus icon otherwise.
|
||||
*/
|
||||
updateSelectChildrenButtons: function($allItems) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
$allItems.filter('.has-children').each(function() {
|
||||
var $item = $(this);
|
||||
var $btn = $item.find('.btn-select-children');
|
||||
if (!$btn.length) return;
|
||||
|
||||
var descendants = self.findTreeDescendants($item, $allItems);
|
||||
|
||||
// Check if parent and ALL descendants are selected
|
||||
var allSelected = $item.hasClass('selected');
|
||||
for (var i = 0; i < descendants.length && allSelected; i++) {
|
||||
if (!$(descendants[i]).hasClass('selected')) {
|
||||
allSelected = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update button icon and title
|
||||
if (allSelected && descendants.length > 0) {
|
||||
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
|
||||
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
|
||||
} else {
|
||||
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
|
||||
$btn.attr('title', trans.select_with_children || 'Select with all children');
|
||||
}
|
||||
});
|
||||
},
|
||||
// NOTE: Tree methods (loadCategoryTree, renderCategoryTree, filterCategoryTree,
|
||||
// findTreeDescendants, findTreeAncestors, updateSelectChildrenButtons) are
|
||||
// defined in _tree.js which is merged later and takes precedence.
|
||||
|
||||
// =========================================================================
|
||||
// Search History
|
||||
|
||||
359
sources/js/admin/entity-selector/_tree.js
Normal file
359
sources/js/admin/entity-selector/_tree.js
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* Entity Selector - Category Tree Module
|
||||
* Hierarchical tree view for category selection inside the dropdown
|
||||
* @partial _tree.js
|
||||
*
|
||||
* Features:
|
||||
* - Expand/collapse individual nodes
|
||||
* - Expand all / Collapse all
|
||||
* - Select parent with all children button
|
||||
* - Visual tree with indentation
|
||||
* - Product count display
|
||||
* - Search/filter within tree
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
// Create mixin namespace
|
||||
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
|
||||
|
||||
// Tree mixin
|
||||
window._EntitySelectorMixins.tree = {
|
||||
|
||||
// Tree state
|
||||
treeData: null,
|
||||
treeFlatData: null,
|
||||
|
||||
/**
|
||||
* Load and display category tree in the dropdown
|
||||
* Called when view mode is changed to "tree"
|
||||
*/
|
||||
loadCategoryTree: function() {
|
||||
var self = this;
|
||||
var $results = this.$dropdown.find('.dropdown-results');
|
||||
var trans = this.config.trans || {};
|
||||
var searchEntity = this.activeGroup ? this.activeGroup.searchEntity : 'categories';
|
||||
|
||||
// Show loading
|
||||
$results.html('<div class="tree-loading"><i class="icon-spinner icon-spin"></i> ' +
|
||||
this.escapeHtml(trans.loading || 'Loading...') + '</div>');
|
||||
|
||||
// Fetch tree data
|
||||
$.ajax({
|
||||
url: this.config.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
ajax: 1,
|
||||
action: 'getCategoryTree',
|
||||
trait: 'EntitySelector',
|
||||
entity_type: searchEntity
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.categories && response.categories.length > 0) {
|
||||
self.treeFlatData = response.categories;
|
||||
self.treeData = self.buildTreeStructure(response.categories);
|
||||
self.renderCategoryTree($results, searchEntity);
|
||||
} else {
|
||||
$results.html('<div class="dropdown-empty">' +
|
||||
self.escapeHtml(trans.no_categories || 'No categories found') + '</div>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$results.html('<div class="dropdown-error">' +
|
||||
self.escapeHtml(trans.error_loading || 'Failed to load categories') + '</div>');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Build nested tree structure from flat array
|
||||
* @param {Array} flatData - Flat array with parent_id references
|
||||
* @returns {Array} Nested tree structure
|
||||
*/
|
||||
buildTreeStructure: function(flatData) {
|
||||
var lookup = {};
|
||||
var tree = [];
|
||||
|
||||
// Create lookup and initialize children arrays
|
||||
flatData.forEach(function(item) {
|
||||
lookup[item.id] = $.extend({}, item, { children: [] });
|
||||
});
|
||||
|
||||
// Build tree by assigning children to parents
|
||||
flatData.forEach(function(item) {
|
||||
var node = lookup[item.id];
|
||||
var parentId = parseInt(item.parent_id, 10);
|
||||
|
||||
if (parentId && lookup[parentId]) {
|
||||
lookup[parentId].children.push(node);
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the category tree inside dropdown results
|
||||
* @param {jQuery} $container - The dropdown-results container
|
||||
* @param {string} entityType - 'categories' or 'cms_categories'
|
||||
*/
|
||||
renderCategoryTree: function($container, entityType) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
// Get currently selected IDs from chips
|
||||
var selectedIds = this.getSelectedIdsFromChips();
|
||||
|
||||
// Build tree HTML
|
||||
var html = '<div class="category-tree" data-entity-type="' + this.escapeAttr(entityType) + '">';
|
||||
|
||||
// Tree toolbar
|
||||
html += '<div class="tree-toolbar">';
|
||||
html += '<button type="button" class="btn-expand-all" title="' +
|
||||
this.escapeAttr(trans.expand_all || 'Expand all') + '">';
|
||||
html += '<i class="icon-plus-square-o"></i> ' + this.escapeHtml(trans.expand_all || 'Expand all');
|
||||
html += '</button>';
|
||||
html += '<button type="button" class="btn-collapse-all" title="' +
|
||||
this.escapeAttr(trans.collapse_all || 'Collapse all') + '">';
|
||||
html += '<i class="icon-minus-square-o"></i> ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
|
||||
// Tree items
|
||||
html += '<div class="tree-items">';
|
||||
html += this.renderTreeItems(this.treeData, 0, selectedIds);
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
$container.html(html);
|
||||
|
||||
// Update count
|
||||
var totalCount = this.treeFlatData ? this.treeFlatData.length : 0;
|
||||
var selectedCount = selectedIds.length;
|
||||
var categoryLabel = entityType === 'cms_categories' ? 'CMS categories' : 'categories';
|
||||
var countText = totalCount + ' ' + categoryLabel;
|
||||
if (selectedCount > 0) {
|
||||
countText += ' (' + selectedCount + ' selected)';
|
||||
}
|
||||
this.$dropdown.find('.results-count').text(countText);
|
||||
|
||||
// Update select children button states
|
||||
this.updateSelectChildrenButtons(this.$dropdown.find('.tree-item'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Render tree items recursively
|
||||
* @param {Array} nodes - Tree nodes
|
||||
* @param {number} level - Current depth level
|
||||
* @param {Array} selectedIds - Currently selected IDs
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
renderTreeItems: function(nodes, level, selectedIds) {
|
||||
var self = this;
|
||||
var html = '';
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
nodes.forEach(function(node) {
|
||||
var hasChildren = node.children && node.children.length > 0;
|
||||
var isSelected = selectedIds.indexOf(parseInt(node.id, 10)) !== -1;
|
||||
var indent = level * 20;
|
||||
|
||||
var itemClass = 'tree-item';
|
||||
if (hasChildren) itemClass += ' has-children';
|
||||
if (isSelected) itemClass += ' selected';
|
||||
if (!node.active) itemClass += ' inactive';
|
||||
|
||||
html += '<div class="' + itemClass + '" data-id="' + node.id + '" ';
|
||||
html += 'data-name="' + self.escapeAttr(node.name) + '" ';
|
||||
html += 'data-level="' + level + '" ';
|
||||
html += 'data-parent-id="' + (node.parent_id || 0) + '">';
|
||||
|
||||
// Indentation
|
||||
html += '<span class="tree-indent" style="width: ' + indent + 'px;"></span>';
|
||||
|
||||
// Toggle button (expand/collapse)
|
||||
if (hasChildren) {
|
||||
html += '<span class="tree-toggle"><i class="icon-caret-down"></i></span>';
|
||||
// Select with children button (next to toggle on the left)
|
||||
html += '<button type="button" class="btn-select-children" title="' +
|
||||
self.escapeAttr(trans.select_with_children || 'Select with all children') + '">';
|
||||
html += '<i class="icon-check-square-o"></i>';
|
||||
html += '</button>';
|
||||
} else {
|
||||
html += '<span class="tree-toggle tree-leaf"></span>';
|
||||
}
|
||||
|
||||
// Checkbox indicator
|
||||
html += '<span class="tree-checkbox"><i class="icon-check"></i></span>';
|
||||
|
||||
// Category icon
|
||||
html += '<span class="tree-icon"><i class="icon-folder"></i></span>';
|
||||
|
||||
// Name
|
||||
html += '<span class="tree-name">' + self.escapeHtml(node.name) + '</span>';
|
||||
|
||||
// Product/page count with clickable preview
|
||||
var itemCount = node.product_count || node.page_count || 0;
|
||||
if (itemCount > 0) {
|
||||
var countLabel = node.page_count ? (trans.pages || 'pages') : (trans.products || 'products');
|
||||
html += '<span class="tree-count clickable" data-category-id="' + node.id + '" ';
|
||||
html += 'title="' + self.escapeAttr(itemCount + ' ' + countLabel) + '">';
|
||||
html += '<i class="icon-eye"></i> ' + itemCount;
|
||||
html += '</span>';
|
||||
}
|
||||
|
||||
// Inactive badge
|
||||
if (!node.active) {
|
||||
html += '<span class="tree-badge inactive">' +
|
||||
self.escapeHtml(trans.inactive || 'Inactive') + '</span>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Render children
|
||||
if (hasChildren) {
|
||||
html += '<div class="tree-children">';
|
||||
html += self.renderTreeItems(node.children, level + 1, selectedIds);
|
||||
html += '</div>';
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get selected IDs from the current picker's chips
|
||||
* @returns {Array} Array of selected IDs
|
||||
*/
|
||||
getSelectedIdsFromChips: function() {
|
||||
var selectedIds = [];
|
||||
|
||||
if (!this.activeGroup) return selectedIds;
|
||||
|
||||
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
|
||||
var $picker;
|
||||
|
||||
if (this.activeGroup.section === 'include') {
|
||||
$picker = $group.find('.include-picker');
|
||||
} else {
|
||||
var $excludeRow = $group.find('.exclude-row[data-exclude-index="' + this.activeGroup.excludeIndex + '"]');
|
||||
$picker = $excludeRow.find('.exclude-picker');
|
||||
}
|
||||
|
||||
$picker.find('.entity-chip').each(function() {
|
||||
selectedIds.push(parseInt($(this).data('id'), 10));
|
||||
});
|
||||
|
||||
return selectedIds;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter category tree by search query
|
||||
* @param {string} query - Search query
|
||||
*/
|
||||
filterCategoryTree: function(query) {
|
||||
var $tree = this.$dropdown.find('.category-tree');
|
||||
if (!$tree.length) return;
|
||||
|
||||
var $items = $tree.find('.tree-item');
|
||||
var $children = $tree.find('.tree-children');
|
||||
query = (query || '').toLowerCase().trim();
|
||||
|
||||
// Remove any inline display styles set by jQuery .toggle()
|
||||
$items.css('display', '');
|
||||
|
||||
if (!query) {
|
||||
$items.removeClass('filtered-out filter-match');
|
||||
$children.removeClass('filter-expanded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark all as filtered out first
|
||||
$items.addClass('filtered-out').removeClass('filter-match');
|
||||
|
||||
// Find matching items and show them with their parents
|
||||
$items.each(function() {
|
||||
var $item = $(this);
|
||||
var name = ($item.data('name') || '').toLowerCase();
|
||||
|
||||
if (name.indexOf(query) !== -1) {
|
||||
$item.removeClass('filtered-out');
|
||||
|
||||
// Show parent containers
|
||||
$item.parents('.tree-children').addClass('filter-expanded');
|
||||
$item.parents('.tree-item').removeClass('filtered-out');
|
||||
|
||||
// Show children of matching item
|
||||
$item.next('.tree-children').find('.tree-item').removeClass('filtered-out');
|
||||
$item.next('.tree-children').addClass('filter-expanded');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Find all descendant tree items of a given item
|
||||
* @param {jQuery} $item - Parent tree item
|
||||
* @param {jQuery} $allItems - All tree items (for performance)
|
||||
* @returns {Array} Array of descendant jQuery elements
|
||||
*/
|
||||
findTreeDescendants: function($item, $allItems) {
|
||||
var descendants = [];
|
||||
var parentId = parseInt($item.data('id'), 10);
|
||||
var level = parseInt($item.data('level'), 10);
|
||||
|
||||
// Find immediate children first
|
||||
var $next = $item.next('.tree-children');
|
||||
if ($next.length) {
|
||||
$next.find('.tree-item').each(function() {
|
||||
descendants.push(this);
|
||||
});
|
||||
}
|
||||
|
||||
return descendants;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the state of select-children buttons based on selection
|
||||
* @param {jQuery} $allItems - All tree items
|
||||
*/
|
||||
updateSelectChildrenButtons: function($allItems) {
|
||||
var self = this;
|
||||
var trans = this.config.trans || {};
|
||||
|
||||
$allItems.filter('.has-children').each(function() {
|
||||
var $item = $(this);
|
||||
var $btn = $item.find('.btn-select-children');
|
||||
if (!$btn.length) return;
|
||||
|
||||
var $children = $item.next('.tree-children');
|
||||
if (!$children.length) return;
|
||||
|
||||
var $childItems = $children.find('.tree-item');
|
||||
var isParentSelected = $item.hasClass('selected');
|
||||
var allChildrenSelected = true;
|
||||
|
||||
$childItems.each(function() {
|
||||
if (!$(this).hasClass('selected')) {
|
||||
allChildrenSelected = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isParentSelected && allChildrenSelected) {
|
||||
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
|
||||
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
|
||||
} else {
|
||||
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
|
||||
$btn.attr('title', trans.select_with_children || 'Select with all children');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
@@ -151,6 +151,13 @@
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if entity type supports tree browsing
|
||||
*/
|
||||
supportsTreeBrowsing: function(entityType) {
|
||||
return entityType === 'categories' || entityType === 'cms_categories';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -196,6 +196,12 @@
|
||||
box-shadow: 0 2px 8px rgba($bg, 0.4);
|
||||
}
|
||||
|
||||
// Focus state - maintain styled appearance
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba($bg, 0.3), 0 2px 8px rgba($bg, 0.4);
|
||||
}
|
||||
|
||||
// Loading state - spinner icon replaces eye
|
||||
&.loading {
|
||||
cursor: wait;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/**
|
||||
* Entity Selector Variables
|
||||
* Bootstrap 4 compatible values for PrestaShop admin theme
|
||||
*
|
||||
* Imports shared variables from prestashop-admin package
|
||||
* and maps them to $es-* prefixed aliases for this package
|
||||
*/
|
||||
|
||||
// Import shared variables from prestashop-admin
|
||||
@use '../../../prestashop-admin/assets/scss/variables' as admin;
|
||||
|
||||
// =============================================================================
|
||||
// Base Colors
|
||||
// =============================================================================
|
||||
@@ -10,38 +16,38 @@
|
||||
$es-white: #ffffff !default;
|
||||
$es-black: #000000 !default;
|
||||
|
||||
// Primary (PrestaShop admin accent)
|
||||
$es-primary: #25b9d7 !default;
|
||||
// Primary (from prestashop-admin)
|
||||
$es-primary: admin.$primary !default;
|
||||
$es-primary-hover: #1a9ab7 !default;
|
||||
$es-primary-light: rgba(37, 185, 215, 0.1) !default;
|
||||
|
||||
// Semantic colors (Bootstrap 4 aligned)
|
||||
$es-success: #28a745 !default;
|
||||
// Semantic colors (from prestashop-admin)
|
||||
$es-success: admin.$success !default;
|
||||
$es-success-light: #d4edda !default;
|
||||
$es-success-dark: #1e7e34 !default;
|
||||
|
||||
$es-danger: #dc3545 !default;
|
||||
$es-danger: admin.$danger !default;
|
||||
$es-danger-light: #f8d7da !default;
|
||||
$es-danger-dark: #bd2130 !default;
|
||||
|
||||
$es-warning: #ffc107 !default;
|
||||
$es-warning: admin.$warning !default;
|
||||
$es-warning-light: #fff3cd !default;
|
||||
|
||||
$es-info: #17a2b8 !default;
|
||||
$es-info: admin.$info !default;
|
||||
$es-info-light: #d1ecf1 !default;
|
||||
|
||||
// =============================================================================
|
||||
// Gray Scale (Bootstrap 4)
|
||||
// =============================================================================
|
||||
|
||||
$es-gray-100: #f8f9fa !default;
|
||||
$es-gray-100: admin.$light !default;
|
||||
$es-gray-200: #e9ecef !default;
|
||||
$es-gray-300: #dee2e6 !default;
|
||||
$es-gray-300: admin.$border-color !default;
|
||||
$es-gray-400: #ced4da !default;
|
||||
$es-gray-500: #adb5bd !default;
|
||||
$es-gray-600: #6c757d !default;
|
||||
$es-gray-600: admin.$secondary !default;
|
||||
$es-gray-700: #495057 !default;
|
||||
$es-gray-800: #343a40 !default;
|
||||
$es-gray-800: admin.$dark !default;
|
||||
$es-gray-900: #212529 !default;
|
||||
|
||||
// Slate (subtle variations)
|
||||
@@ -73,7 +79,7 @@ $es-bg-hover: $es-gray-200 !default;
|
||||
$es-bg-active: $es-gray-200 !default;
|
||||
$es-bg-body: $es-white !default;
|
||||
|
||||
$es-border-color: $es-gray-300 !default;
|
||||
$es-border-color: admin.$border-color !default;
|
||||
$es-border-light: $es-gray-200 !default;
|
||||
$es-border-dark: $es-gray-400 !default;
|
||||
|
||||
@@ -83,22 +89,22 @@ $es-text-muted: $es-gray-600 !default;
|
||||
$es-text-light: $es-gray-500 !default;
|
||||
|
||||
// =============================================================================
|
||||
// Spacing (Bootstrap 4 compatible)
|
||||
// Spacing (Bootstrap 4 compatible, derived from admin.$spacer)
|
||||
// =============================================================================
|
||||
|
||||
$es-spacing-xs: 0.25rem !default; // 4px
|
||||
$es-spacing-sm: 0.5rem !default; // 8px
|
||||
$es-spacing-md: 1rem !default; // 16px
|
||||
$es-spacing-lg: 1.5rem !default; // 24px
|
||||
$es-spacing-xl: 2rem !default; // 32px
|
||||
$es-spacing-xs: admin.$spacer * 0.25 !default; // 4px
|
||||
$es-spacing-sm: admin.$spacer * 0.5 !default; // 8px
|
||||
$es-spacing-md: admin.$spacer !default; // 16px
|
||||
$es-spacing-lg: admin.$spacer * 1.5 !default; // 24px
|
||||
$es-spacing-xl: admin.$spacer * 2 !default; // 32px
|
||||
|
||||
// =============================================================================
|
||||
// Border Radius (Bootstrap 4 compatible)
|
||||
// Border Radius (from prestashop-admin)
|
||||
// =============================================================================
|
||||
|
||||
$es-radius-sm: 0.2rem !default;
|
||||
$es-radius-md: 0.25rem !default;
|
||||
$es-radius-lg: 0.3rem !default;
|
||||
$es-radius-sm: admin.$border-radius-sm !default;
|
||||
$es-radius-md: admin.$border-radius !default;
|
||||
$es-radius-lg: admin.$border-radius-lg !default;
|
||||
$es-radius-xl: 0.5rem !default;
|
||||
$es-radius-full: 50rem !default;
|
||||
|
||||
|
||||
@@ -9,19 +9,169 @@
|
||||
.target-conditions-trait,
|
||||
.entity-selector-trait {
|
||||
|
||||
// Chips container wrapper with toolbar
|
||||
.chips-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: $es-spacing-sm;
|
||||
background: $es-slate-50;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-md;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Chips toolbar - search and actions
|
||||
.chips-toolbar {
|
||||
display: none; // Hidden by default, shown via JS when chips exist
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-white;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
|
||||
&.has-chips {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// Search icon
|
||||
> i {
|
||||
color: $es-text-muted;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chips-search-input,
|
||||
input.chips-search-input,
|
||||
input.chips-search-input[type="text"] {
|
||||
flex: 1 !important;
|
||||
min-width: 80px !important;
|
||||
max-width: none !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid transparent !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
font-size: $es-font-size-sm !important;
|
||||
color: $es-text-primary;
|
||||
box-shadow: none !important;
|
||||
transition: border-color $es-transition-fast;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid $es-primary !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.chips-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
background: $es-primary;
|
||||
color: $es-white;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
border-radius: $es-radius-full;
|
||||
white-space: nowrap;
|
||||
|
||||
&.has-filter {
|
||||
background: $es-cyan-500;
|
||||
}
|
||||
|
||||
.count-filtered {
|
||||
font-weight: $es-font-weight-bold;
|
||||
}
|
||||
|
||||
.count-separator {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.chips-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-xs;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-chips-clear {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
color: $es-white;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
background: $es-danger;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: darken($es-danger, 8%);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
// Chips container
|
||||
.entity-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $es-spacing-xs;
|
||||
padding: $es-spacing-sm 0;
|
||||
min-height: 32px;
|
||||
padding: $es-spacing-md;
|
||||
min-height: 40px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Load more button
|
||||
.chips-load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-white;
|
||||
border-top: 1px solid $es-border-color;
|
||||
|
||||
.btn-load-more {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: $es-white;
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
background: $es-primary;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-hover;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Individual chip
|
||||
.entity-chip {
|
||||
display: inline-flex;
|
||||
@@ -33,7 +183,6 @@
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-medium;
|
||||
border-radius: $es-radius-full;
|
||||
max-width: 200px;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
@@ -44,6 +193,12 @@
|
||||
&.has-image {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
// Hidden by search filter or pagination
|
||||
&.chip-filtered-out,
|
||||
&.chip-paginated-out {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chip-image {
|
||||
@@ -55,14 +210,26 @@
|
||||
}
|
||||
|
||||
.chip-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: $es-text-muted;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Product/entity images inside chip
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: cover;
|
||||
border-radius: $es-radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.chip-text,
|
||||
.chip-name {
|
||||
@include text-truncate;
|
||||
// Show full name, no truncation
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chip-remove {
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
// Results container
|
||||
.dropdown-results {
|
||||
padding: $es-spacing-sm;
|
||||
padding: 0 $es-spacing-sm;
|
||||
}
|
||||
|
||||
// Results count text
|
||||
@@ -609,80 +609,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tree-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $es-text-muted;
|
||||
cursor: pointer;
|
||||
transition: transform $es-transition-fast;
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-toggle-placeholder {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-select-children {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $es-primary;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-light;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-select-children-placeholder {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.tree-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid $es-border-dark;
|
||||
border-radius: 3px;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
i {
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
color: $es-white;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
color: $es-text-muted;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
// tree-toggle, btn-select-children, tree-checkbox, tree-icon styles in _tree.scss
|
||||
|
||||
.tree-info {
|
||||
display: flex;
|
||||
@@ -1521,14 +1448,11 @@ body > .target-search-dropdown,
|
||||
color: $es-text-muted;
|
||||
}
|
||||
|
||||
.dropdown-footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
}
|
||||
|
||||
.btn-cancel-dropdown {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: $es-font-size-sm;
|
||||
color: $es-text-secondary;
|
||||
@@ -1539,6 +1463,12 @@ body > .target-search-dropdown,
|
||||
|
||||
&:hover {
|
||||
background: $es-bg-hover;
|
||||
color: $es-danger;
|
||||
border-color: $es-danger;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
@@ -1566,6 +1496,11 @@ body > .target-search-dropdown,
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-hover;
|
||||
border-color: $es-primary-hover;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
@@ -1702,7 +1637,7 @@ body > .target-search-dropdown,
|
||||
|
||||
// Results container
|
||||
.dropdown-results {
|
||||
padding: $es-spacing-sm;
|
||||
padding: 0 $es-spacing-sm;
|
||||
background: $es-white;
|
||||
min-height: 200px;
|
||||
}
|
||||
@@ -1768,16 +1703,16 @@ body > .target-search-dropdown,
|
||||
}
|
||||
}
|
||||
|
||||
// View mode classes (applied to dropdown container)
|
||||
&.view-cols-2 .dropdown-results { @include grid-columns(2); }
|
||||
&.view-cols-3 .dropdown-results { @include grid-columns(3); }
|
||||
&.view-cols-4 .dropdown-results { @include grid-columns(4); }
|
||||
&.view-cols-5 .dropdown-results { @include grid-columns(5); }
|
||||
&.view-cols-6 .dropdown-results { @include grid-columns(6); }
|
||||
&.view-cols-7 .dropdown-results { @include grid-columns(7); }
|
||||
&.view-cols-8 .dropdown-results { @include grid-columns(8); }
|
||||
// View mode classes (applied to dropdown container) - no gap/padding for shared borders
|
||||
&.view-cols-2 .dropdown-results { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
&.view-cols-3 .dropdown-results { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
&.view-cols-4 .dropdown-results { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
&.view-cols-5 .dropdown-results { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
&.view-cols-6 .dropdown-results { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
&.view-cols-7 .dropdown-results { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
&.view-cols-8 .dropdown-results { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0; padding: 0; border-top: 1px solid $es-border-color; border-left: 1px solid $es-border-color; }
|
||||
|
||||
// Grid view item styling (compact cards)
|
||||
// Grid view item styling (compact cards with shared borders)
|
||||
&.view-cols-2,
|
||||
&.view-cols-3,
|
||||
&.view-cols-4,
|
||||
@@ -1789,9 +1724,11 @@ body > .target-search-dropdown,
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-sm;
|
||||
padding: $es-spacing-sm;
|
||||
border: none;
|
||||
border-right: 1px solid $es-border-color;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
border-radius: 0;
|
||||
|
||||
.result-checkbox {
|
||||
position: absolute;
|
||||
@@ -1861,6 +1798,15 @@ body > .target-search-dropdown,
|
||||
}
|
||||
}
|
||||
|
||||
// Remove right border from last item in each row (per column count)
|
||||
&.view-cols-2 .dropdown-results .dropdown-item:nth-child(2n) { border-right: none; }
|
||||
&.view-cols-3 .dropdown-results .dropdown-item:nth-child(3n) { border-right: none; }
|
||||
&.view-cols-4 .dropdown-results .dropdown-item:nth-child(4n) { border-right: none; }
|
||||
&.view-cols-5 .dropdown-results .dropdown-item:nth-child(5n) { border-right: none; }
|
||||
&.view-cols-6 .dropdown-results .dropdown-item:nth-child(6n) { border-right: none; }
|
||||
&.view-cols-7 .dropdown-results .dropdown-item:nth-child(7n) { border-right: none; }
|
||||
&.view-cols-8 .dropdown-results .dropdown-item:nth-child(8n) { border-right: none; }
|
||||
|
||||
// Smaller items for higher column counts
|
||||
&.view-cols-5,
|
||||
&.view-cols-6,
|
||||
@@ -2175,80 +2121,7 @@ body > .target-search-dropdown,
|
||||
}
|
||||
}
|
||||
|
||||
.tree-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $es-text-muted;
|
||||
cursor: pointer;
|
||||
transition: transform $es-transition-fast;
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-toggle-placeholder {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-select-children {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $es-primary;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-primary-light;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-select-children-placeholder {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.tree-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid $es-border-dark;
|
||||
border-radius: 3px;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
i {
|
||||
display: none;
|
||||
font-size: 10px;
|
||||
color: $es-white;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
color: $es-text-muted;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
// tree-toggle, btn-select-children, tree-checkbox, tree-icon styles in _tree.scss
|
||||
|
||||
.tree-info {
|
||||
display: flex;
|
||||
@@ -2280,7 +2153,7 @@ body > .target-search-dropdown,
|
||||
.dropdown-results {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: $es-spacing-sm;
|
||||
padding: 0 $es-spacing-sm;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
@@ -2498,6 +2371,7 @@ body > .target-search-dropdown,
|
||||
.entity-search-icon {
|
||||
color: $es-text-muted;
|
||||
flex-shrink: 0;
|
||||
margin-left: $es-spacing-xs;
|
||||
}
|
||||
|
||||
// Override Bootstrap/parent form input styles
|
||||
|
||||
@@ -319,22 +319,13 @@
|
||||
border-top: 1px dashed $es-border-color;
|
||||
}
|
||||
|
||||
// Legacy exclude-rows (if used elsewhere)
|
||||
.exclude-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $es-spacing-sm;
|
||||
}
|
||||
|
||||
.exclude-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm;
|
||||
background: rgba($es-danger, 0.05);
|
||||
border: 1px solid rgba($es-danger, 0.2);
|
||||
border-radius: $es-radius-md;
|
||||
}
|
||||
|
||||
.exclude-row-content {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -534,9 +525,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Group include section
|
||||
// Group include section - green accent to distinguish from exclude
|
||||
.group-include {
|
||||
margin-bottom: $es-spacing-md;
|
||||
padding: $es-spacing-sm;
|
||||
background: rgba($es-success, 0.03);
|
||||
border: 1px solid rgba($es-success, 0.2);
|
||||
border-radius: $es-radius-md;
|
||||
}
|
||||
|
||||
.section-row {
|
||||
@@ -565,32 +560,81 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Lock indicator for method selector (when excludes are present)
|
||||
.selector-locked {
|
||||
.include-method-select {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.lock-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: $es-warning;
|
||||
cursor: help;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mpr-tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: $es-spacing-xs $es-spacing-sm;
|
||||
background: $es-slate-800;
|
||||
color: $es-white;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-normal;
|
||||
white-space: nowrap;
|
||||
border-radius: $es-radius-sm;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
&:hover .mpr-tooltip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Group excludes section
|
||||
.group-excludes {
|
||||
margin-top: $es-spacing-md;
|
||||
|
||||
&.has-excludes {
|
||||
padding-top: $es-spacing-md;
|
||||
border-top: 1px dashed $es-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.except-separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $es-spacing-sm;
|
||||
gap: $es-spacing-sm;
|
||||
margin: 0 0 $es-spacing-sm 0;
|
||||
|
||||
// Lines on both sides
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: rgba($es-danger, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.except-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: $es-danger-light;
|
||||
color: $es-danger;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
border-radius: $es-radius-sm;
|
||||
border-radius: $es-radius-full;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
@@ -604,17 +648,36 @@
|
||||
}
|
||||
|
||||
.exclude-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $es-spacing-sm;
|
||||
background: rgba($es-danger, 0.03);
|
||||
border: 1px solid rgba($es-danger, 0.15);
|
||||
border-radius: $es-radius-md;
|
||||
|
||||
// Value picker inside exclude row - full width
|
||||
.value-picker {
|
||||
width: 100%;
|
||||
margin-top: $es-spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.exclude-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $es-spacing-sm;
|
||||
margin-bottom: $es-spacing-sm;
|
||||
width: 100%;
|
||||
|
||||
.method-selector-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Delete button at the far right
|
||||
.btn-remove-exclude-row {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-remove-exclude-row {
|
||||
@@ -680,9 +743,14 @@
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
// Common height for all modifier controls
|
||||
$modifier-height: 26px;
|
||||
|
||||
.group-modifier-limit {
|
||||
width: 60px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: 50px;
|
||||
max-width: 50px;
|
||||
height: $modifier-height;
|
||||
padding: 0 0.375rem;
|
||||
font-size: $es-font-size-xs;
|
||||
text-align: center;
|
||||
border: 1px solid $es-border-color;
|
||||
@@ -694,8 +762,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Sort modifier - input group style (select + button glued together)
|
||||
.modifier-sort {
|
||||
gap: 0; // Remove gap to glue select + button together
|
||||
|
||||
.modifier-label {
|
||||
margin-right: 0.375rem; // Keep space between label and input group
|
||||
}
|
||||
|
||||
.group-modifier-sort {
|
||||
height: $modifier-height;
|
||||
padding: 0 0.5rem;
|
||||
font-size: $es-font-size-xs;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-sm 0 0 $es-radius-sm;
|
||||
border-right: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
border-color: $es-primary;
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sort-dir {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $modifier-height;
|
||||
height: $modifier-height;
|
||||
color: $es-text-muted;
|
||||
background: $es-slate-100;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: 0 $es-radius-sm $es-radius-sm 0;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-secondary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for elements outside .modifier-sort context
|
||||
.group-modifier-sort {
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: $modifier-height;
|
||||
padding: 0 0.5rem;
|
||||
font-size: $es-font-size-xs;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-sm;
|
||||
@@ -712,8 +831,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: $modifier-height;
|
||||
height: $modifier-height;
|
||||
color: $es-text-muted;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-sm;
|
||||
|
||||
@@ -494,3 +494,92 @@
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Total Summary Popover (header total badge click)
|
||||
// =============================================================================
|
||||
|
||||
.total-preview-popover {
|
||||
min-width: 240px;
|
||||
max-width: 320px;
|
||||
|
||||
.preview-popover-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
background: $es-bg-header;
|
||||
border-bottom: 1px solid $es-border-color;
|
||||
|
||||
.preview-popover-title {
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-text-primary;
|
||||
font-size: $es-font-size-sm;
|
||||
}
|
||||
|
||||
.preview-popover-count {
|
||||
font-size: $es-font-size-xs;
|
||||
color: $es-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-popover-body {
|
||||
padding: $es-spacing-xs 0;
|
||||
}
|
||||
|
||||
.total-summary-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.total-summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-sm $es-spacing-md;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-50;
|
||||
}
|
||||
|
||||
i {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
color: $es-text-muted;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-item-label {
|
||||
flex: 1;
|
||||
font-size: $es-font-size-sm;
|
||||
color: $es-text-primary;
|
||||
}
|
||||
|
||||
.summary-item-count {
|
||||
font-size: $es-font-size-sm;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-primary;
|
||||
background: rgba($es-primary, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: $es-radius-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make trait-total-count clickable
|
||||
.trait-total-count {
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.popover-open {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,14 +82,25 @@
|
||||
|
||||
// Hidden select (for form submission)
|
||||
.method-select-hidden {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Global fallback for hidden method selects
|
||||
.method-select-hidden {
|
||||
position: absolute !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Method Dropdown Menu (appended to body, outside trait wrappers)
|
||||
// =============================================================================
|
||||
|
||||
@@ -109,44 +109,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MPR Icon (SVG mask icons)
|
||||
// =============================================================================
|
||||
|
||||
.mpr-icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: $es-slate-600;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
|
||||
&:hover {
|
||||
background-color: #5bc0de;
|
||||
}
|
||||
|
||||
&.link {
|
||||
background-color: #5bc0de;
|
||||
|
||||
&:hover {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info icon
|
||||
.mpr-icon.icon-info {
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 2.5H5A2.5 2.5 0 0 0 2.5 5v6A2.5 2.5 0 0 0 5 13.5h6a2.5 2.5 0 0 0 2.5-2.5V5A2.5 2.5 0 0 0 11 2.5ZM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5Z' fill='%23414552'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.25 8A.75.75 0 0 1 7 7.25h1.25A.75.75 0 0 1 9 8v3.5a.75.75 0 0 1-1.5 0V8.75H7A.75.75 0 0 1 6.25 8Z' fill='%23414552'/%3E%3Cpath d='M6.75 5a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0Z' fill='%23414552'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 2.5H5A2.5 2.5 0 0 0 2.5 5v6A2.5 2.5 0 0 0 5 13.5h6a2.5 2.5 0 0 0 2.5-2.5V5A2.5 2.5 0 0 0 11 2.5ZM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5Z' fill='%23414552'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.25 8A.75.75 0 0 1 7 7.25h1.25A.75.75 0 0 1 9 8v3.5a.75.75 0 0 1-1.5 0V8.75H7A.75.75 0 0 1 6.25 8Z' fill='%23414552'/%3E%3Cpath d='M6.75 5a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0Z' fill='%23414552'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tooltip Content Styling
|
||||
// =============================================================================
|
||||
|
||||
342
sources/scss/components/_tree.scss
Normal file
342
sources/scss/components/_tree.scss
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Category Tree Component
|
||||
* Hierarchical tree view for category selection inside dropdown
|
||||
*/
|
||||
|
||||
@use '../variables' as *;
|
||||
@use '../mixins' as *;
|
||||
|
||||
// Category tree container (inside dropdown)
|
||||
.category-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Tree toolbar inside dropdown
|
||||
.category-tree .tree-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-sm;
|
||||
padding: $es-spacing-xs $es-spacing-sm;
|
||||
background: $es-slate-50;
|
||||
border-bottom: 1px solid $es-border-light;
|
||||
flex-shrink: 0;
|
||||
|
||||
.btn-expand-all,
|
||||
.btn-collapse-all {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: $es-spacing-xs $es-spacing-sm;
|
||||
font-size: $es-font-size-xs;
|
||||
font-weight: $es-font-weight-medium;
|
||||
color: $es-text-secondary;
|
||||
background: $es-white;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-100;
|
||||
border-color: $es-slate-300;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tree items container
|
||||
.category-tree .tree-items {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Tree item
|
||||
.tree-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $es-spacing-xs;
|
||||
padding: $es-spacing-xs $es-spacing-sm;
|
||||
cursor: pointer;
|
||||
transition: background $es-transition-fast;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-100;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $es-primary-light;
|
||||
|
||||
.tree-name {
|
||||
font-weight: $es-font-weight-semibold;
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
.tree-checkbox {
|
||||
color: $es-primary;
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
opacity: 0.6;
|
||||
|
||||
.tree-name {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&.filtered-out {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.filter-match {
|
||||
background: $es-warning-light;
|
||||
|
||||
&.selected {
|
||||
background: $es-primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All tree element styles nested under .category-tree for specificity
|
||||
.category-tree {
|
||||
// Tree indentation
|
||||
.tree-indent {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Tree toggle (expand/collapse)
|
||||
.tree-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
color: $es-text-secondary;
|
||||
flex-shrink: 0;
|
||||
border-radius: $es-radius-sm;
|
||||
transition: all $es-transition-fast;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $es-slate-200;
|
||||
color: $es-text-primary;
|
||||
}
|
||||
|
||||
&.tree-leaf {
|
||||
cursor: default;
|
||||
visibility: hidden;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
transition: transform $es-transition-fast;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item.collapsed > .tree-toggle i {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
// Tree checkbox indicator - 12x12 to match PrestaShop admin standards
|
||||
.tree-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid $es-border-color;
|
||||
border-radius: 2px;
|
||||
background: $es-white;
|
||||
|
||||
i {
|
||||
font-size: 8px;
|
||||
opacity: 0;
|
||||
color: $es-white;
|
||||
transition: opacity $es-transition-fast;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item.selected .tree-checkbox {
|
||||
background: $es-primary;
|
||||
border-color: $es-primary;
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Tree icon
|
||||
.tree-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
color: $es-text-muted;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 12px; // match visual weight of other icons
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item.selected .tree-icon {
|
||||
color: $es-primary;
|
||||
}
|
||||
|
||||
// Tree name
|
||||
.tree-name {
|
||||
flex: 1;
|
||||
font-size: $es-font-size-sm;
|
||||
color: $es-text-primary;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Tree product/page count with preview
|
||||
.tree-count {
|
||||
@include count-badge($es-primary);
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
padding: 0 $es-spacing-sm;
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
&.loading {
|
||||
pointer-events: none;
|
||||
|
||||
i {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.popover-open {
|
||||
background: darken($es-primary, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select children button - positioned on the left next to toggle
|
||||
.btn-select-children {
|
||||
@include button-reset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
color: $es-text-muted;
|
||||
border-radius: $es-radius-sm;
|
||||
opacity: 0.3;
|
||||
transition: all $es-transition-fast;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 14px; // larger to visually match other icons
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $es-primary;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item:hover .btn-select-children {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
// Tree badge (inactive, etc.)
|
||||
.tree-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem $es-spacing-xs;
|
||||
font-size: 9px;
|
||||
font-weight: $es-font-weight-semibold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
border-radius: $es-radius-sm;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.inactive {
|
||||
color: $es-warning;
|
||||
background: $es-warning-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Tree children container
|
||||
.tree-children {
|
||||
display: block;
|
||||
|
||||
&.filter-expanded {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item.collapsed + .tree-children {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Filtering - must be inside .category-tree for specificity
|
||||
.tree-item.filtered-out {
|
||||
display: none !important;
|
||||
}
|
||||
} // end .category-tree
|
||||
|
||||
// Loading/empty/error states
|
||||
.category-tree .tree-loading,
|
||||
.category-tree .dropdown-empty,
|
||||
.category-tree .dropdown-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $es-spacing-xl;
|
||||
color: $es-text-muted;
|
||||
font-size: $es-font-size-sm;
|
||||
|
||||
i {
|
||||
margin-right: $es-spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.category-tree .dropdown-error {
|
||||
color: $es-danger;
|
||||
}
|
||||
|
||||
// Tree view mode in dropdown
|
||||
.target-search-dropdown.view-tree {
|
||||
.dropdown-results {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.category-tree {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
.tree-items {
|
||||
max-height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Separation between chips and search box
|
||||
.chips-wrapper + .entity-search-box {
|
||||
margin-top: $es-spacing-md;
|
||||
}
|
||||
|
||||
.entity-search-icon {
|
||||
color: $es-text-muted;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-left: $es-spacing-xs;
|
||||
}
|
||||
|
||||
// Override parent form's max-width on search input
|
||||
@@ -85,6 +91,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Browse tree button (for categories)
|
||||
.btn-browse-tree {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: auto;
|
||||
color: $es-primary;
|
||||
background: $es-primary-light;
|
||||
border-radius: $es-radius-sm;
|
||||
flex-shrink: 0;
|
||||
transition: all $es-transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $es-primary;
|
||||
color: $es-white;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric range box
|
||||
.numeric-range-box,
|
||||
.multi-range-input-row {
|
||||
|
||||
@@ -28,3 +28,4 @@
|
||||
@use 'components/combinations';
|
||||
@use 'components/method-dropdown';
|
||||
@use 'components/tooltip';
|
||||
@use 'components/tree';
|
||||
|
||||
Reference in New Issue
Block a user