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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,184 @@
/**
* MPR Search Revolution - Universal Modal Styles
* All admin modals use .mpr-sr-admin-modal class
* Works on both Symfony pages (.show) and Legacy pages (.in)
*/
.mpr-sr-admin-modal.show,
.mpr-sr-admin-modal.in {
display: flex !important;
align-items: center;
justify-content: center;
}
.mpr-sr-admin-modal.show .modal-dialog,
.mpr-sr-admin-modal.in .modal-dialog {
transform: none !important;
top: auto !important;
margin: 0 auto !important;
}
.mpr-sr-admin-modal .modal-dialog {
max-width: 480px;
}
.mpr-sr-admin-modal .modal-content {
padding: 1.25rem;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.mpr-sr-admin-modal .modal-header,
.mpr-sr-admin-modal .modal-body,
.mpr-sr-admin-modal .modal-footer {
padding: 0;
}
.mpr-sr-admin-modal .modal-header {
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
border: none;
}
.mpr-sr-admin-modal .modal-header .modal-title {
font-size: 1.125rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.mpr-sr-admin-modal .modal-header .modal-title i {
margin-right: 8px;
}
.mpr-sr-admin-modal .modal-header .close,
.mpr-sr-admin-modal .modal-header .mpr-close-modal {
display: flex;
align-items: center;
justify-content: center;
margin: 0;
margin-left: auto;
padding: 5px;
background: transparent;
border: none;
cursor: pointer;
transition: opacity 0.2s;
line-height: 1;
opacity: 0.7;
}
.mpr-sr-admin-modal .modal-header .close:hover,
.mpr-sr-admin-modal .modal-header .mpr-close-modal:hover {
opacity: 1;
}
.mpr-sr-admin-modal .modal-body {
text-align: center;
}
.mpr-sr-admin-modal .modal-body .body-title {
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 0.25rem;
}
.mpr-sr-admin-modal .modal-body .modal-amount {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 1.25rem;
color: #dc2626;
}
.mpr-sr-admin-modal .modal-body .tables-table {
margin-bottom: 1rem;
font-size: 0.875rem;
text-align: left;
}
.mpr-sr-admin-modal .modal-body .tables-table thead th {
background: #f8fafc;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.025em;
color: #64748b;
padding: 0.5rem 0.75rem;
}
.mpr-sr-admin-modal .modal-body .tables-table tbody td {
padding: 0.5rem 0.75rem;
vertical-align: middle;
border-color: #f1f5f9;
}
.mpr-sr-admin-modal .modal-footer {
display: flex;
padding-top: 1rem;
gap: 0.75rem;
border: none;
}
.mpr-sr-admin-modal .modal-footer .btn {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
font-weight: 500;
border-radius: 6px;
position: relative;
z-index: 1;
cursor: pointer;
}
.mpr-sr-admin-modal .modal-footer .btn i {
margin-right: 5px;
}
.mpr-sr-admin-modal .modal-footer .cancel-btn,
.mpr-sr-admin-modal .modal-footer .btn-secondary {
background: #f1f5f9;
border: 1px solid #e2e8f0;
color: #64748b;
}
.mpr-sr-admin-modal .modal-footer .cancel-btn:hover,
.mpr-sr-admin-modal .modal-footer .btn-secondary:hover {
background: #e2e8f0;
color: #475569;
}
.mpr-sr-admin-modal .modal-footer .confirm-btn,
.mpr-sr-admin-modal .modal-footer .btn-primary {
border: none;
color: #fff;
background: linear-gradient(135deg, #337ab7 0%, #286090 100%);
}
.mpr-sr-admin-modal .modal-footer .confirm-btn:hover:not(:disabled),
.mpr-sr-admin-modal .modal-footer .btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
background: linear-gradient(135deg, #286090 0%, #204d74 100%);
}
.mpr-sr-admin-modal .modal-footer .confirm-btn:disabled,
.mpr-sr-admin-modal .modal-footer .btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mpr-sr-admin-modal .modal-footer .btn-danger {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
border: none;
color: #fff;
}
.mpr-sr-admin-modal .modal-footer .btn-danger:hover:not(:disabled) {
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
transform: translateY(-1px);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

515
assets/assets/js/admin/modal.js Executable file
View File

@@ -0,0 +1,515 @@
/**
* MPR Express Checkout - Standardized Modal Helper
*
* Provides a consistent API for managing modals across the module.
*
* Usage:
* const modal = new MPRModal('my-modal-id');
* modal.show();
* modal.setHeader('success', 'icon-check', 'Operation Complete');
* modal.setBody('<p>Content here</p>');
* modal.setFooter([
* { type: 'cancel', label: 'Close' },
* { type: 'primary', label: 'Continue', icon: 'arrow-right', onClick: () => {} }
* ]);
*/
class MPRModal {
/**
* @param {string} modalId - The modal element ID (without #)
* @param {Object} options - Configuration options
* @param {Function} options.onShow - Callback when modal is shown
* @param {Function} options.onHide - Callback when modal is hidden
* @param {Function} options.onCancel - Callback when cancel/close is clicked
*/
constructor(modalId, options = {}) {
this.modalId = modalId;
this.$modal = $(`#${modalId}`);
this.$header = this.$modal.find('.mpr-modal-header');
this.$title = this.$modal.find('.mpr-modal-title');
this.$titleText = this.$modal.find('.mpr-modal-title-text');
this.$titleIcon = this.$modal.find('.mpr-modal-icon');
this.$body = this.$modal.find('.mpr-modal-body');
this.$footer = this.$modal.find('.mpr-modal-footer');
this.options = options;
this.currentView = null;
this._bindEvents();
}
/**
* Bind modal events
*/
_bindEvents() {
this.$modal.on('shown.bs.modal', () => {
if (typeof this.options.onShow === 'function') {
this.options.onShow();
}
});
this.$modal.on('hidden.bs.modal', () => {
if (typeof this.options.onHide === 'function') {
this.options.onHide();
}
});
this.$modal.on('click', '[data-dismiss="modal"]', () => {
if (typeof this.options.onCancel === 'function') {
this.options.onCancel();
}
});
}
/**
* Show the modal
* @param {Object} options - Bootstrap modal options
*/
show(options = {}) {
if (this.$modal.length === 0) {
console.error('[MPRModal] Modal element not found');
return;
}
this.$modal.modal({
backdrop: options.static ? 'static' : true,
keyboard: !options.static,
...options
});
}
/**
* Hide the modal
*/
hide() {
this.$modal.modal('hide');
}
/**
* Set header appearance
* @param {string} type - Header type: default, primary, success, warning, danger, dark
* @param {string} icon - Icon class without prefix (e.g., 'shield' for icon-shield)
* @param {string} title - Title text
*/
setHeader(type, icon, title) {
// Remove all header type classes
this.$header.removeClass(
'mpr-modal-header-default mpr-modal-header-primary mpr-modal-header-success ' +
'mpr-modal-header-warning mpr-modal-header-danger mpr-modal-header-dark'
);
this.$header.addClass(`mpr-modal-header-${type}`);
if (icon) {
if (this.$titleIcon.length) {
this.$titleIcon.attr('class', `mpr-icon icon-${icon} mpr-modal-icon`);
} else {
this.$title.prepend(`<i class="mpr-icon icon-${icon} mpr-modal-icon"></i>`);
this.$titleIcon = this.$modal.find('.mpr-modal-icon');
}
}
if (title !== undefined) {
this.$titleText.text(title);
}
}
/**
* Set only the header type/color
* @param {string} type - Header type: default, primary, success, warning, danger, dark
*/
setHeaderType(type) {
this.$header.removeClass(
'mpr-modal-header-default mpr-modal-header-primary mpr-modal-header-success ' +
'mpr-modal-header-warning mpr-modal-header-danger mpr-modal-header-dark'
);
this.$header.addClass(`mpr-modal-header-${type}`);
}
/**
* Set the header title
* @param {string} title - Title text
*/
setTitle(title) {
this.$titleText.text(title);
}
/**
* Set the header icon
* @param {string} icon - Icon class without prefix
*/
setIcon(icon) {
if (this.$titleIcon.length) {
this.$titleIcon.attr('class', `icon-${icon} mpr-modal-icon`);
}
}
/**
* Set modal size
* @param {string} size - Size: sm, md, lg, xl, fullwidth
*/
setSize(size) {
const $dialog = this.$modal.find('.modal-dialog');
$dialog.removeClass('modal-sm modal-lg modal-xl modal-fullwidth');
if (size === 'sm') {
$dialog.addClass('modal-sm');
} else if (size === 'lg') {
$dialog.addClass('modal-lg');
} else if (size === 'xl') {
$dialog.addClass('modal-xl');
} else if (size === 'fullwidth') {
$dialog.addClass('modal-fullwidth');
}
}
/**
* Set body content
* @param {string} html - HTML content for the body
*/
setBody(html) {
this.$body.html(html);
}
/**
* Append content to body
* @param {string} html - HTML content to append
*/
appendBody(html) {
this.$body.append(html);
}
/**
* Set footer buttons
* @param {Array} buttons - Array of button configurations
* Each button: { type, label, icon, id, onClick, disabled, className, size }
* type: 'cancel', 'primary', 'success', 'warning', 'danger', 'default', 'dark',
* 'outline-primary', 'outline-danger', 'ghost'
* size: 'sm', 'lg' (optional)
*/
setFooter(buttons) {
this.$footer.empty();
buttons.forEach(btn => {
const btnType = btn.type === 'cancel' ? 'default' : btn.type;
let btnClass = `mpr-btn mpr-btn-${btnType}`;
if (btn.size) {
btnClass += ` mpr-btn-${btn.size}`;
}
if (btn.className) {
btnClass += ` ${btn.className}`;
}
const $btn = $('<button>', {
type: 'button',
class: btnClass,
id: btn.id || undefined,
disabled: btn.disabled || false
});
if (btn.type === 'cancel' || btn.dismiss) {
$btn.attr('data-dismiss', 'modal');
}
// Add icon - cancel buttons get 'close' icon by default if no icon specified
const iconName = btn.icon || (btn.type === 'cancel' ? 'close' : null);
if (iconName) {
$btn.append(`<i class="mpr-icon icon-${iconName}"></i> `);
}
// Support HTML in labels (for inline icons) or plain text
if (btn.html) {
$btn.append(btn.label);
} else {
$btn.append(document.createTextNode(btn.label));
}
if (typeof btn.onClick === 'function') {
$btn.on('click', btn.onClick);
}
this.$footer.append($btn);
});
}
/**
* Set button loading state
* @param {string} buttonId - Button ID
* @param {boolean} loading - Loading state
*/
setButtonLoading(buttonId, loading) {
const $btn = $(`#${buttonId}`);
if (loading) {
$btn.addClass('mpr-btn-loading').prop('disabled', true);
} else {
$btn.removeClass('mpr-btn-loading').prop('disabled', false);
}
}
/**
* Show footer
*/
showFooter() {
this.$footer.removeClass('hidden');
}
/**
* Hide footer
*/
hideFooter() {
this.$footer.addClass('hidden');
}
/**
* Enable/disable a footer button by ID
* @param {string} buttonId - Button ID
* @param {boolean} enabled - Enable or disable
*/
setButtonEnabled(buttonId, enabled) {
$(`#${buttonId}`).prop('disabled', !enabled);
}
/**
* Update button label
* @param {string} buttonId - Button ID
* @param {string} label - New label
* @param {string} icon - Optional new icon
*/
setButtonLabel(buttonId, label, icon = null) {
const $btn = $(`#${buttonId}`);
$btn.empty();
if (icon) {
$btn.append(`<i class="mpr-icon icon-${icon}"></i> `);
}
$btn.append(document.createTextNode(label));
}
/**
* Switch between views (for multi-step modals)
* Views should have class 'mpr-modal-view' and a data-view attribute
* @param {string} viewName - The view to show
*/
showView(viewName) {
this.$body.find('.mpr-modal-view').removeClass('active');
this.$body.find(`[data-view="${viewName}"]`).addClass('active');
this.currentView = viewName;
}
/**
* Get current view name
* @returns {string|null}
*/
getCurrentView() {
return this.currentView;
}
/**
* Create and show a simple confirmation modal
* @param {Object} config - Configuration
* @param {string} config.type - Header type
* @param {string} config.icon - Header icon
* @param {string} config.title - Title
* @param {string} config.message - Body message (can be HTML)
* @param {string} config.confirmLabel - Confirm button label
* @param {string} config.confirmType - Confirm button type (primary, danger, etc.)
* @param {string} config.cancelLabel - Cancel button label
* @param {Function} config.onConfirm - Confirm callback
* @param {Function} config.onCancel - Cancel callback
*/
confirm(config) {
this.setHeader(
config.type || 'primary',
config.icon || 'question',
config.title || 'Confirm'
);
this.setBody(`
<div class="mpr-modal-center">
<p>${config.message}</p>
</div>
`);
this.setFooter([
{
type: 'cancel',
label: config.cancelLabel || 'Cancel',
onClick: config.onCancel
},
{
type: config.confirmType || 'primary',
label: config.confirmLabel || 'Confirm',
icon: config.confirmIcon,
onClick: () => {
if (typeof config.onConfirm === 'function') {
config.onConfirm();
}
if (config.autoClose !== false) {
this.hide();
}
}
}
]);
this.show({ static: config.static || false });
}
/**
* Show a progress state
* @param {Object} config - Configuration
* @param {string} config.title - Progress title
* @param {string} config.subtitle - Progress subtitle
* @param {number} config.percent - Initial percentage (0-100)
*/
showProgress(config = {}) {
const title = config.title || 'Processing...';
const subtitle = config.subtitle || 'Please wait';
const percent = config.percent || 0;
this.setBody(`
<div class="mpr-modal-progress">
<i class="icon-refresh mpr-modal-progress-icon"></i>
<div class="mpr-modal-progress-title">${title}</div>
<div class="mpr-modal-progress-subtitle">${subtitle}</div>
<div class="mpr-modal-progress-bar-container">
<div class="mpr-modal-progress-bar" style="width: ${percent}%"></div>
</div>
<div class="mpr-modal-progress-percent">${percent}%</div>
<div class="mpr-modal-progress-current"></div>
</div>
`);
this.hideFooter();
this.setHeaderType('primary');
}
/**
* Update progress bar
* @param {number} percent - Percentage (0-100)
* @param {string} currentItem - Current item being processed
*/
updateProgress(percent, currentItem = '') {
this.$body.find('.mpr-modal-progress-bar').css('width', `${percent}%`);
this.$body.find('.mpr-modal-progress-percent').text(`${Math.round(percent)}%`);
if (currentItem) {
this.$body.find('.mpr-modal-progress-current').text(currentItem);
}
}
/**
* Show a result state
* @param {Object} config - Configuration
* @param {string} config.type - Result type: success, warning, danger, info
* @param {string} config.icon - Icon (defaults based on type)
* @param {string} config.title - Result title
* @param {string} config.message - Result message
* @param {string} config.closeLabel - Close button label
* @param {Function} config.onClose - Close callback
*/
showResult(config) {
const iconMap = {
success: 'check-circle',
warning: 'warning',
danger: 'times-circle',
info: 'info-circle'
};
const icon = config.icon || iconMap[config.type] || 'info-circle';
this.setHeaderType(config.type === 'info' ? 'primary' : config.type);
this.setBody(`
<div class="mpr-modal-result">
<i class="icon-${icon} mpr-modal-result-icon result-${config.type}"></i>
<div class="mpr-modal-result-title">${config.title}</div>
<div class="mpr-modal-result-message">${config.message}</div>
</div>
`);
this.setFooter([
{
type: 'primary',
label: config.closeLabel || 'Close',
onClick: () => {
if (typeof config.onClose === 'function') {
config.onClose();
}
this.hide();
}
}
]);
this.showFooter();
}
/**
* Lock modal (prevent closing)
*/
lock() {
this.$modal.data('bs.modal').options.backdrop = 'static';
this.$modal.data('bs.modal').options.keyboard = false;
this.$modal.find('.mpr-modal-close').hide();
}
/**
* Unlock modal (allow closing)
*/
unlock() {
this.$modal.data('bs.modal').options.backdrop = true;
this.$modal.data('bs.modal').options.keyboard = true;
this.$modal.find('.mpr-modal-close').show();
}
/**
* Destroy the modal instance
*/
destroy() {
this.$modal.modal('dispose');
this.$modal.off();
}
}
/**
* Factory function to create modals dynamically
* Creates the modal HTML and appends it to the body
*
* @param {Object} config - Modal configuration
* @param {string} config.id - Modal ID
* @param {string} config.size - Modal size: sm, md, lg, xl
* @param {boolean} config.static - Static backdrop
* @returns {MPRModal}
*/
MPRModal.create = function(config) {
const id = config.id || 'mpr-modal-' + Date.now();
const sizeClass = config.size === 'sm' ? 'modal-sm' :
config.size === 'lg' ? 'modal-lg' :
config.size === 'xl' ? 'modal-xl' :
config.size === 'fullwidth' ? 'modal-fullwidth' : '';
const html = `
<div class="modal fade mpr-modal" id="${id}" tabindex="-1" role="dialog"
${config.static ? 'data-backdrop="static" data-keyboard="false"' : ''}>
<div class="modal-dialog ${sizeClass}" role="document">
<div class="modal-content">
<div class="modal-header mpr-modal-header mpr-modal-header-primary">
<h5 class="modal-title mpr-modal-title">
<i class="mpr-modal-icon"></i>
<span class="mpr-modal-title-text"></span>
</h5>
<button type="button" class="close mpr-modal-close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body mpr-modal-body"></div>
<div class="modal-footer mpr-modal-footer"></div>
</div>
</div>
</div>
`;
$('body').append(html);
return new MPRModal(id, config);
};
// Export for module systems if available
if (typeof module !== 'undefined' && module.exports) {
module.exports = MPRModal;
}

View File

@@ -0,0 +1,3 @@
// Placeholder - _core.js
// Placeholder - _timeline.js

View File

@@ -0,0 +1,5 @@
// Placeholder - _core.js
// Placeholder - _timeline.js
//# sourceMappingURL=schedule-conditions.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"schedule-conditions.min.js","sourcesContent":[]}

View File

@@ -16,6 +16,13 @@
/**
* MPR Admin Variables
* @package prestashop-admin
*
* Color token system:
* - PS theme vars (--primary, --danger, etc.) → set by PrestaShop admin theme
* - MPR tokens (--mpr-*) → set here as defaults, overridable per module/theme
*
* Usage in SCSS: var(--token, $fallback)
* The $fallback SCSS vars below ensure compilation succeeds even without :root.
*/
/**
* Entity Selector Styles
@@ -36,6 +43,27 @@
transform: rotate(360deg);
}
}
@keyframes spin-pulse {
0% {
transform: rotate(0deg);
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
transform: rotate(360deg);
opacity: 1;
}
}
.es-spin {
animation: spin 1s linear infinite;
}
.es-spin-pulse {
animation: spin-pulse 1s ease-in-out infinite;
}
/**
* Entity Selector Styles
* @package prestashop-entity-selector
@@ -251,6 +279,10 @@
border: 1px solid #dee2e6;
border-radius: 0.3rem;
}
.target-conditions-trait .material-icons,
.entity-selector-trait .material-icons {
font-size: 18px !important;
}
.target-conditions-trait .condition-trait-header,
.entity-selector-trait .condition-trait-header {
display: flex;
@@ -433,7 +465,7 @@
}
.target-conditions-trait .trait-show-all-toggle .show-all-checkbox:checked + .toggle-slider,
.entity-selector-trait .trait-show-all-toggle .show-all-checkbox:checked + .toggle-slider {
background: #28a745;
background: #70b580;
}
.target-conditions-trait .trait-show-all-toggle .show-all-checkbox:checked + .toggle-slider::after,
.entity-selector-trait .trait-show-all-toggle .show-all-checkbox:checked + .toggle-slider::after {
@@ -514,9 +546,9 @@
border-bottom-color: #06b6d4;
color: #25b9d7;
}
.target-conditions-trait .target-block-tab i,
.entity-selector-trait .target-block-tab i {
font-size: 0.875rem;
.target-conditions-trait .target-block-tab i.material-icons,
.entity-selector-trait .target-block-tab i.material-icons {
font-size: 18px !important;
}
.target-conditions-trait .target-block-tab .tab-label,
.entity-selector-trait .target-block-tab .tab-label {
@@ -682,6 +714,38 @@
border-bottom: 0;
border-radius: 0.3rem 0 0 0;
}
.target-conditions-trait .entity-selector-actions:not(.btn-toggle-blocks),
.entity-selector-trait .entity-selector-actions:not(.btn-toggle-blocks) {
display: flex;
align-items: center;
padding: 0.25rem 1rem;
border-left: 1px solid #dee2e6;
}
.target-conditions-trait .entity-selector-actions:not(.btn-toggle-blocks) .btn-toggle-groups,
.entity-selector-trait .entity-selector-actions:not(.btn-toggle-blocks) .btn-toggle-groups {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
background: #ffffff;
border: 1px solid #94a3b8;
border-radius: 0.25rem;
color: #334155;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.target-conditions-trait .entity-selector-actions:not(.btn-toggle-blocks) .btn-toggle-groups:hover,
.entity-selector-trait .entity-selector-actions:not(.btn-toggle-blocks) .btn-toggle-groups:hover {
background: rgba(37, 185, 215, 0.1);
color: #25b9d7;
border-color: #25b9d7;
}
.target-conditions-trait .entity-selector-actions:not(.btn-toggle-blocks) .btn-toggle-groups .material-icons,
.entity-selector-trait .entity-selector-actions:not(.btn-toggle-blocks) .btn-toggle-groups .material-icons {
font-size: 18px !important;
}
.target-conditions-trait .entity-selector-actions.btn-toggle-blocks,
.entity-selector-trait .entity-selector-actions.btn-toggle-blocks {
display: flex;
@@ -749,7 +813,7 @@
}
.target-conditions-trait .target-block-empty i,
.entity-selector-trait .target-block-empty i {
font-size: 2rem;
font-size: 2rem !important;
opacity: 0.5;
}
.target-conditions-trait .target-block-empty p,
@@ -799,10 +863,6 @@
.entity-selector-trait.single-mode .target-block-container {
display: block;
}
.target-conditions-trait.single-mode .entity-selector-tabs-row .target-block-tabs,
.entity-selector-trait.single-mode .entity-selector-tabs-row .target-block-tabs {
display: flex;
}
.target-conditions-trait .header-actions,
.entity-selector-trait .header-actions {
@@ -844,7 +904,7 @@
}
.target-conditions-trait .header-action-btn i,
.entity-selector-trait .header-action-btn i {
font-size: 12px;
font-size: 14px !important;
}
/**
@@ -1688,11 +1748,11 @@
}
.target-conditions-trait .result-col-stock .col-value.stock-ok,
.entity-selector-trait .result-col-stock .col-value.stock-ok {
color: #28a745;
color: #70b580;
}
.target-conditions-trait .result-col-stock .col-value.stock-low,
.entity-selector-trait .result-col-stock .col-value.stock-low {
color: #ffc107;
color: #fab000;
}
.target-conditions-trait .result-col-stock .col-value.stock-out,
.entity-selector-trait .result-col-stock .col-value.stock-out {
@@ -1753,19 +1813,6 @@
align-items: center;
gap: 0.5rem;
}
.target-conditions-trait .dropdown-item.is-combination,
.entity-selector-trait .dropdown-item.is-combination {
padding-left: 28px;
}
.target-conditions-trait .dropdown-item.is-combination .result-name,
.entity-selector-trait .dropdown-item.is-combination .result-name {
font-size: 0.9em;
}
.target-conditions-trait .dropdown-item.is-parent-product,
.entity-selector-trait .dropdown-item.is-parent-product {
background: #f8fafc;
font-weight: 500;
}
.target-conditions-trait .no-results,
.entity-selector-trait .no-results {
display: flex;
@@ -3044,12 +3091,12 @@ body > .target-search-dropdown .filter-group-toggle.active .toggle-count,
}
body > .target-search-dropdown .filter-group-toggle.has-selection,
.target-search-dropdown .filter-group-toggle.has-selection {
border-color: #28a745;
background: rgba(40, 167, 69, 0.05);
border-color: #70b580;
background: rgba(112, 181, 128, 0.05);
}
body > .target-search-dropdown .filter-group-toggle.has-selection .toggle-count,
.target-search-dropdown .filter-group-toggle.has-selection .toggle-count {
color: #28a745;
color: #70b580;
font-weight: 600;
}
body > .target-search-dropdown .filter-group-toggle .toggle-name,
@@ -3058,9 +3105,46 @@ body > .target-search-dropdown .filter-group-toggle .toggle-name,
}
body > .target-search-dropdown .filter-group-toggle .toggle-count,
.target-search-dropdown .filter-group-toggle .toggle-count {
display: inline-flex;
align-items: center;
gap: 0.125rem;
color: #6c757d;
font-size: 0.65rem;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count i,
.target-search-dropdown .filter-group-toggle .toggle-count i {
font-size: 10px;
color: #25b9d7;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count.clickable,
.target-search-dropdown .filter-group-toggle .toggle-count.clickable {
cursor: pointer;
padding: 0.125rem 0.25rem;
border-radius: 0.2rem;
transition: all 0.15s ease-in-out;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count.clickable:hover,
.target-search-dropdown .filter-group-toggle .toggle-count.clickable:hover {
background: rgba(37, 185, 215, 0.1);
color: #25b9d7;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count.clickable:hover i,
.target-search-dropdown .filter-group-toggle .toggle-count.clickable:hover i {
color: #25b9d7;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count.clickable.popover-open,
.target-search-dropdown .filter-group-toggle .toggle-count.clickable.popover-open {
background: #25b9d7;
color: #ffffff;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count.clickable.popover-open i,
.target-search-dropdown .filter-group-toggle .toggle-count.clickable.popover-open i {
color: #ffffff;
}
body > .target-search-dropdown .filter-group-toggle .toggle-count.clickable.loading i,
.target-search-dropdown .filter-group-toggle .toggle-count.clickable.loading i {
animation: spin 0.6s linear infinite;
}
body > .target-search-dropdown .filter-chip, body > .target-search-dropdown .filter-attr-chip,
body > .target-search-dropdown .filter-feat-chip,
.target-search-dropdown .filter-chip,
@@ -3181,81 +3265,6 @@ body > .target-search-dropdown .filter-chip.active .chip-count,
.target-search-dropdown .active.filter-feat-chip .chip-count {
color: rgba(255, 255, 255, 0.8);
}
body > .target-search-dropdown .filter-chip-wrapper,
.target-search-dropdown .filter-chip-wrapper {
display: inline-flex;
align-items: stretch;
border-radius: 0.2rem;
overflow: hidden;
}
body > .target-search-dropdown .filter-chip-wrapper .filter-chip,
body > .target-search-dropdown .filter-chip-wrapper .filter-group-toggle,
.target-search-dropdown .filter-chip-wrapper .filter-chip,
.target-search-dropdown .filter-chip-wrapper .filter-attr-chip,
.target-search-dropdown .filter-chip-wrapper .filter-feat-chip,
.target-search-dropdown .filter-chip-wrapper .filter-group-toggle {
border-radius: 0.2rem 0 0 0.2rem;
}
body > .target-search-dropdown .filter-chip-wrapper .chip-preview-btn,
.target-search-dropdown .filter-chip-wrapper .chip-preview-btn {
padding: 0;
margin: 0;
background: none;
border: none;
cursor: pointer;
font: inherit;
color: inherit;
}
body > .target-search-dropdown .filter-chip-wrapper .chip-preview-btn:focus,
.target-search-dropdown .filter-chip-wrapper .chip-preview-btn:focus {
outline: none;
}
body > .target-search-dropdown .filter-chip-wrapper .chip-preview-btn,
.target-search-dropdown .filter-chip-wrapper .chip-preview-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 0.375rem;
font-size: 10px;
color: #6c757d;
background: #f1f5f9;
border-left: 1px solid #dee2e6;
border-radius: 0 0.2rem 0.2rem 0;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
body > .target-search-dropdown .filter-chip-wrapper .chip-preview-btn:hover,
.target-search-dropdown .filter-chip-wrapper .chip-preview-btn:hover {
background: rgba(37, 185, 215, 0.1);
color: #25b9d7;
}
body > .target-search-dropdown .filter-chip-wrapper .chip-preview-btn.popover-open,
.target-search-dropdown .filter-chip-wrapper .chip-preview-btn.popover-open {
background: #25b9d7;
color: #ffffff;
}
body > .target-search-dropdown .filter-chip-wrapper .chip-preview-btn.loading i,
.target-search-dropdown .filter-chip-wrapper .chip-preview-btn.loading i {
animation: spin 0.6s linear infinite;
}
body > .target-search-dropdown .filter-chip-wrapper .filter-chip:last-child,
body > .target-search-dropdown .filter-chip-wrapper .filter-group-toggle:last-child,
.target-search-dropdown .filter-chip-wrapper .filter-chip:last-child,
.target-search-dropdown .filter-chip-wrapper .filter-attr-chip:last-child,
.target-search-dropdown .filter-chip-wrapper .filter-feat-chip:last-child,
.target-search-dropdown .filter-chip-wrapper .filter-group-toggle:last-child {
border-radius: 0.2rem;
}
body > .target-search-dropdown .filter-chip-wrapper .filter-group-toggle.active + .chip-preview-btn,
.target-search-dropdown .filter-chip-wrapper .filter-group-toggle.active + .chip-preview-btn {
border-left-color: #25b9d7;
background: rgba(37, 185, 215, 0.05);
}
body > .target-search-dropdown .filter-chip-wrapper .filter-group-toggle.has-selection + .chip-preview-btn,
.target-search-dropdown .filter-chip-wrapper .filter-group-toggle.has-selection + .chip-preview-btn {
border-left-color: #28a745;
background: rgba(40, 167, 69, 0.03);
}
body > .target-search-dropdown .dropdown-content,
.target-search-dropdown .dropdown-content {
max-height: 400px;
@@ -3982,7 +3991,7 @@ body > .target-search-dropdown.view-cols-2 .dropdown-item .result-grid-info .gri
.target-search-dropdown.view-cols-6 .dropdown-item .result-grid-info .grid-stock.stock-low,
.target-search-dropdown.view-cols-7 .dropdown-item .result-grid-info .grid-stock.stock-low,
.target-search-dropdown.view-cols-8 .dropdown-item .result-grid-info .grid-stock.stock-low {
color: #ffc107;
color: #fab000;
}
body > .target-search-dropdown.view-cols-2 .dropdown-item .result-grid-info .grid-discount, body > .target-search-dropdown.view-cols-3 .dropdown-item .result-grid-info .grid-discount, body > .target-search-dropdown.view-cols-4 .dropdown-item .result-grid-info .grid-discount, body > .target-search-dropdown.view-cols-5 .dropdown-item .result-grid-info .grid-discount, body > .target-search-dropdown.view-cols-6 .dropdown-item .result-grid-info .grid-discount, body > .target-search-dropdown.view-cols-7 .dropdown-item .result-grid-info .grid-discount, body > .target-search-dropdown.view-cols-8 .dropdown-item .result-grid-info .grid-discount,
.target-search-dropdown.view-cols-2 .dropdown-item .result-grid-info .grid-discount,
@@ -3992,7 +4001,7 @@ body > .target-search-dropdown.view-cols-2 .dropdown-item .result-grid-info .gri
.target-search-dropdown.view-cols-6 .dropdown-item .result-grid-info .grid-discount,
.target-search-dropdown.view-cols-7 .dropdown-item .result-grid-info .grid-discount,
.target-search-dropdown.view-cols-8 .dropdown-item .result-grid-info .grid-discount {
color: #28a745;
color: #70b580;
font-weight: 500;
}
body > .target-search-dropdown.view-cols-2 .results-header, body > .target-search-dropdown.view-cols-3 .results-header, body > .target-search-dropdown.view-cols-4 .results-header, body > .target-search-dropdown.view-cols-5 .results-header, body > .target-search-dropdown.view-cols-6 .results-header, body > .target-search-dropdown.view-cols-7 .results-header, body > .target-search-dropdown.view-cols-8 .results-header,
@@ -4157,11 +4166,11 @@ body > .target-search-dropdown .result-col-sale,
}
body > .target-search-dropdown .result-col-stock .col-value.stock-ok,
.target-search-dropdown .result-col-stock .col-value.stock-ok {
color: #28a745;
color: #70b580;
}
body > .target-search-dropdown .result-col-stock .col-value.stock-low,
.target-search-dropdown .result-col-stock .col-value.stock-low {
color: #ffc107;
color: #fab000;
}
body > .target-search-dropdown .result-col-stock .col-value.stock-out,
.target-search-dropdown .result-col-stock .col-value.stock-out {
@@ -4505,10 +4514,10 @@ body > .target-search-dropdown .tree-subtitle,
font-weight: 600;
}
.target-search-dropdown .result-col-stock .col-value.stock-ok {
color: #28a745;
color: #70b580;
}
.target-search-dropdown .result-col-stock .col-value.stock-low {
color: #ffc107;
color: #fab000;
}
.target-search-dropdown .result-col-stock .col-value.stock-out {
color: #dc3545;
@@ -5029,7 +5038,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .chip-preview-holidays i.material-icons,
.entity-selector-trait .chip-preview-holidays i.material-icons {
font-size: 14px;
font-size: 14px !important;
}
.target-conditions-trait .chip-text,
.target-conditions-trait .chip-name,
@@ -5037,16 +5046,6 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
.entity-selector-trait .chip-name {
word-break: break-word;
}
.target-conditions-trait .chip-attrs,
.entity-selector-trait .chip-attrs {
font-size: 0.85em;
opacity: 0.7;
margin-left: 2px;
}
.target-conditions-trait .chip-attrs::before,
.entity-selector-trait .chip-attrs::before {
content: "— ";
}
.target-conditions-trait .chip-remove,
.entity-selector-trait .chip-remove {
padding: 0;
@@ -5099,7 +5098,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .entity-chip.chip-success:hover,
.entity-selector-trait .entity-chip.chip-success:hover {
background: rgba(40, 167, 69, 0.2);
background: rgba(112, 181, 128, 0.2);
}
.target-conditions-trait .entity-chip.chip-danger,
.entity-selector-trait .entity-chip.chip-danger {
@@ -5113,11 +5112,11 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
.target-conditions-trait .entity-chip.chip-warning,
.entity-selector-trait .entity-chip.chip-warning {
background: #fff3cd;
color: #a07800;
color: rgb(148, 104.192, 0);
}
.target-conditions-trait .entity-chip.chip-warning:hover,
.entity-selector-trait .entity-chip.chip-warning:hover {
background: rgba(255, 193, 7, 0.3);
background: rgba(250, 176, 0, 0.3);
}
.target-conditions-trait .entity-chip.loading,
.target-conditions-trait .entity-chip-loading,
@@ -5362,7 +5361,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .pattern-tag.case-sensitive .case-icon,
.entity-selector-trait .pattern-tag.case-sensitive .case-icon {
color: #28a745;
color: #70b580;
font-weight: 700;
}
.target-conditions-trait .pattern-tag.draft-tag,
@@ -5518,11 +5517,11 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .pattern-match-count.count-zero,
.entity-selector-trait .pattern-match-count.count-zero {
color: #ffc107;
color: #fab000;
}
.target-conditions-trait .pattern-match-count.count-found,
.entity-selector-trait .pattern-match-count.count-found {
color: #28a745;
color: #70b580;
}
.target-conditions-trait .pattern-match-count .count-value,
.entity-selector-trait .pattern-match-count .count-value {
@@ -5593,7 +5592,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
color: #495057;
}
.holiday-preview-popover .popover-close i.material-icons {
font-size: 18px;
font-size: 18px !important;
}
.holiday-preview-popover .popover-body {
max-height: 350px;
@@ -5625,9 +5624,9 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
font-size: 0.875rem;
}
.holiday-preview-popover .holiday-preview-loading i.material-icons {
font-size: 20px;
font-size: 20px !important;
}
.holiday-preview-popover .holiday-preview-loading .icon-spin {
.holiday-preview-popover .holiday-preview-loading .es-spin {
animation: spin 1s linear infinite;
}
.holiday-preview-popover .holiday-preview-empty {
@@ -5636,7 +5635,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
color: #6c757d;
}
.holiday-preview-popover .holiday-preview-empty i.material-icons {
font-size: 48px;
font-size: 48px !important;
opacity: 0.4;
margin-bottom: 0.5rem;
display: block;
@@ -5657,13 +5656,13 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
padding: 0.5rem 1rem;
background: #f8fafc;
border-radius: 0.25rem;
border-left: 3px solid #28a745;
border-left: 3px solid #70b580;
}
.holiday-preview-popover .holiday-item.holiday-type-bank, .holiday-preview-popover .holiday-item.holiday-type-bank-holiday {
border-left-color: #17a2b8;
}
.holiday-preview-popover .holiday-item.holiday-type-observance {
border-left-color: #ffc107;
border-left-color: #fab000;
}
.holiday-preview-popover .holiday-item.holiday-type-regional, .holiday-preview-popover .holiday-item.holiday-type-local-holiday {
border-left-color: #8b5cf6;
@@ -5726,7 +5725,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
background: #f8fafc;
}
.holiday-preview-popover .popover-filter i.material-icons {
font-size: 18px;
font-size: 18px !important;
color: #6c757d;
}
.holiday-preview-popover .popover-filter .holiday-filter-input {
@@ -5951,7 +5950,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .section-label.label-include i,
.entity-selector-trait .section-label.label-include i {
color: #28a745;
color: #70b580;
}
.target-conditions-trait .section-label.label-exclude,
.entity-selector-trait .section-label.label-exclude {
@@ -6475,11 +6474,12 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
width: 24px;
color: #6c757d;
}
.target-conditions-trait .group-collapse-toggle i,
.entity-selector-trait .group-collapse-toggle i {
font-size: 20px !important;
transition: transform 0.15s ease-in-out;
}
.target-conditions-trait .group-name-wrapper,
@@ -6620,8 +6620,8 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
.entity-selector-trait .group-include {
margin-bottom: 1rem;
padding: 0.5rem;
background: rgba(40, 167, 69, 0.03);
border: 1px solid rgba(40, 167, 69, 0.2);
background: rgba(112, 181, 128, 0.03);
border: 1px solid rgba(112, 181, 128, 0.2);
border-radius: 0.25rem;
}
.target-conditions-trait .section-row,
@@ -6689,12 +6689,12 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
justify-content: center;
width: 24px;
height: 24px;
color: #ffc107;
color: #fab000;
cursor: help;
}
.target-conditions-trait .lock-indicator i,
.entity-selector-trait .lock-indicator i {
font-size: 14px;
font-size: 16px !important;
}
.target-conditions-trait .lock-indicator .mpr-tooltip,
.entity-selector-trait .lock-indicator .mpr-tooltip {
@@ -6751,7 +6751,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .except-label i,
.entity-selector-trait .except-label i {
font-size: 10px;
font-size: 12px !important;
}
.target-conditions-trait .exclude-rows-container,
.entity-selector-trait .exclude-rows-container {
@@ -6866,7 +6866,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
.target-conditions-trait .btn-add-another-exclude i,
.entity-selector-trait .btn-add-exclude i,
.entity-selector-trait .btn-add-another-exclude i {
font-size: 10px;
font-size: 12px !important;
}
.target-conditions-trait .group-modifiers,
.entity-selector-trait .group-modifiers {
@@ -6882,20 +6882,23 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .modifier-inline,
.entity-selector-trait .modifier-inline {
display: flex;
display: inline-flex;
align-items: center;
gap: 0.375rem;
flex-shrink: 0;
}
.target-conditions-trait .group-modifier-limit,
.entity-selector-trait .group-modifier-limit {
width: 50px;
max-width: 50px;
min-width: 50px;
height: 26px;
padding: 0 0.375rem;
font-size: 0.75rem;
text-align: center;
border: 1px solid #dee2e6;
border-radius: 0.2rem;
box-sizing: border-box;
}
.target-conditions-trait .group-modifier-limit:focus,
.entity-selector-trait .group-modifier-limit:focus {
@@ -6912,13 +6915,15 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .modifier-sort .group-modifier-sort,
.entity-selector-trait .modifier-sort .group-modifier-sort {
width: auto;
height: 26px;
padding: 0 0.5rem;
padding: 0 1.25rem 0 0.5rem;
font-size: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.2rem 0 0 0.2rem;
border-right: none;
cursor: pointer;
box-sizing: border-box;
}
.target-conditions-trait .modifier-sort .group-modifier-sort:focus,
.entity-selector-trait .modifier-sort .group-modifier-sort:focus {
@@ -6961,7 +6966,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .modifier-sort .btn-sort-dir i,
.entity-selector-trait .modifier-sort .btn-sort-dir i {
font-size: 11px;
font-size: 14px !important;
}
.target-conditions-trait .group-modifier-sort,
.entity-selector-trait .group-modifier-sort {
@@ -7135,42 +7140,6 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
.target-conditions-trait[data-mode=single] .groups-container,
.target-conditions-trait .mode-single .groups-container,
.entity-selector-trait[data-mode=single] .groups-container,
.entity-selector-trait .mode-single .groups-container {
padding: 0;
}
.target-conditions-trait[data-mode=single] .group-body,
.target-conditions-trait .mode-single .group-body,
.entity-selector-trait[data-mode=single] .group-body,
.entity-selector-trait .mode-single .group-body {
padding: 0;
}
.target-conditions-trait[data-mode=single] .group-include,
.target-conditions-trait .mode-single .group-include,
.entity-selector-trait[data-mode=single] .group-include,
.entity-selector-trait .mode-single .group-include {
padding: 0.5rem;
margin-bottom: 0;
background: transparent;
border: none;
border-radius: 0;
}
.target-conditions-trait[data-mode=single] .selection-group,
.target-conditions-trait .mode-single .selection-group,
.entity-selector-trait[data-mode=single] .selection-group,
.entity-selector-trait .mode-single .selection-group {
background: transparent;
border: none;
margin-bottom: 0;
}
.target-conditions-trait[data-mode=single] .group-header,
.target-conditions-trait .mode-single .group-header,
.entity-selector-trait[data-mode=single] .group-header,
.entity-selector-trait .mode-single .group-header {
display: none;
}
.target-conditions-trait .condition-match-count,
.entity-selector-trait .condition-match-count {
display: inline-flex;
@@ -7209,7 +7178,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.target-conditions-trait .condition-match-count i,
.entity-selector-trait .condition-match-count i {
font-size: 10px;
font-size: 12px !important;
}
/**
@@ -8075,7 +8044,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
margin: 0;
}
#mpr-holiday-preview-modal .mpr-modal-title i.material-icons {
font-size: 20px;
font-size: 20px !important;
color: #25b9d7;
}
#mpr-holiday-preview-modal .mpr-modal-close {
@@ -8145,7 +8114,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
color: #6c757d;
}
#mpr-holiday-preview-modal .holiday-preview-empty i.material-icons {
font-size: 48px;
font-size: 48px !important;
opacity: 0.5;
margin-bottom: 1rem;
}
@@ -8168,13 +8137,13 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
padding: 0.5rem 1rem;
background: #f8fafc;
border-radius: 0.25rem;
border-left: 3px solid #28a745;
border-left: 3px solid #70b580;
}
#mpr-holiday-preview-modal .holiday-item.holiday-type-bank {
border-left-color: #17a2b8;
}
#mpr-holiday-preview-modal .holiday-item.holiday-type-observance {
border-left-color: #ffc107;
border-left-color: #fab000;
}
#mpr-holiday-preview-modal .holiday-item.holiday-type-regional {
border-left-color: #8b5cf6;
@@ -8283,20 +8252,6 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
right: 20px;
transform: none;
}
.target-preview-popover.position-above::before,
.target-list-preview-popover.position-above::before {
top: auto;
bottom: -8px;
border-top: 8px solid #dee2e6;
border-bottom: 0;
}
.target-preview-popover.position-above::after,
.target-list-preview-popover.position-above::after {
top: auto;
bottom: -6px;
border-top: 6px solid #ffffff;
border-bottom: 0;
}
.preview-header {
display: flex;
@@ -8734,7 +8689,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
.mpr-dropdown-preview__item > i:first-child {
flex-shrink: 0;
width: 16px;
font-size: 14px;
font-size: 14px !important;
color: #999;
text-align: center;
}
@@ -8780,15 +8735,11 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
cursor: pointer;
text-transform: none;
}
.mpr-badge--preview .icon-eye {
font-size: 10px;
.mpr-badge--preview .material-icons {
font-size: 12px !important;
line-height: 1;
opacity: 0.8;
}
.mpr-badge--preview .material-icons {
font-size: 12px;
line-height: 1;
}
.mpr-badge--preview-primary {
background: #25b9d7;
@@ -9130,7 +9081,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
gap: 0.5rem;
}
.schedule-summary .summary-item i {
color: #28a745;
color: #70b580;
font-size: 0.875rem;
}
.schedule-summary .summary-item.inactive i {
@@ -9165,7 +9116,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
}
.schedule-toggle-row .schedule-toggle-actions .material-icons {
color: #94a3b8;
font-size: 20px;
font-size: 20px !important;
}
.schedule-summary-badges {
@@ -9189,7 +9140,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
white-space: nowrap;
}
.schedule-badge .material-icons {
font-size: 14px;
font-size: 14px !important;
opacity: 0.7;
}
@@ -9233,7 +9184,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
.target-conditions-trait .tips-header > i:first-child,
.entity-selector-trait .tips-header > i:first-child {
font-size: 1rem;
color: #ffc107;
color: #fab000;
}
.target-conditions-trait .tips-header > span,
.entity-selector-trait .tips-header > span {
@@ -10395,7 +10346,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
margin-left: 0.25rem;
}
.mpr-info-wrapper .material-icons {
font-size: 16px;
font-size: 16px !important;
color: #6c757d;
transition: color 0.15s ease;
}
@@ -10455,7 +10406,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
transition: background-color 0.15s ease;
}
.mpr-tooltip-close .material-icons {
font-size: 16px;
font-size: 16px !important;
color: #6c757d;
}
.mpr-tooltip-close:hover {
@@ -10779,7 +10730,7 @@ body > .target-search-dropdown .dropdown-item:not(:last-child) {
flex-shrink: 0;
}
.category-tree .tree-badge.inactive {
color: #ffc107;
color: #fab000;
background: #fff3cd;
}
.category-tree .tree-children {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3400
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -26,10 +26,10 @@
// Select all / Clear buttons with keyboard shortcuts
html += '<button type="button" class="btn-select-all" title="' + (trans.select_all || 'Select all visible') + '">';
html += '<i class="icon-check-square-o"></i> ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += this.esIcon('check_box') + ' ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += '</button>';
html += '<button type="button" class="btn-clear-selection" title="' + (trans.clear_selection || 'Clear selection') + '">';
html += '<i class="icon-square-o"></i> ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += this.esIcon('check_box_outline_blank') + ' ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += '</button>';
// Sort controls - options with data-entities attribute for entity-specific filtering
@@ -50,7 +50,7 @@
html += '<option value="product_count" data-entities="categories,manufacturers,suppliers">' + (trans.sort_product_count || 'Products') + '</option>';
html += '</select>';
html += '<button type="button" class="btn-sort-dir" data-dir="ASC" title="Sort direction">';
html += '<i class="icon-sort-alpha-asc"></i>';
html += this.esIcon('sort_by_alpha');
html += '</button>';
// View mode selector - Tree option always present, shown for categories
@@ -69,19 +69,19 @@
// Refine search
html += '<div class="refine-compact">';
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '"><i class="icon-ban"></i></button>';
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '">' + this.esIcon('block') + '</button>';
html += '<input type="text" class="refine-input" placeholder="' + (trans.refine_short || 'Refine...') + '">';
html += '<button type="button" class="btn-clear-refine" style="display:none;"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-refine" style="display:none;">' + this.esIcon('close') + '</button>';
html += '</div>';
// Filter toggle button
html += '<button type="button" class="btn-toggle-filters" title="' + (trans.toggle_filters || 'Filters') + '">';
html += '<i class="icon-filter"></i>';
html += this.esIcon('filter_list');
html += '</button>';
// History button
html += '<button type="button" class="btn-show-history" title="' + (trans.recent_searches || 'Recent searches') + '">';
html += '<i class="icon-clock-o"></i>';
html += this.esIcon('schedule');
html += '</button>';
html += '</div>'; // End dropdown-actions
@@ -104,13 +104,13 @@
html += '</div>';
html += '<button type="button" class="btn-clear-filters" title="' + (trans.clear_filters || 'Clear filters') + '">';
html += '<i class="icon-times"></i>';
html += this.esIcon('close');
html += '</button>';
html += '</div>';
// Attribute/Feature filter toggles for products
html += '<div class="filter-row filter-row-attributes" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label"><i class="icon-tags"></i> ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<span class="filter-row-label">' + this.esIcon('label') + ' ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<div class="filter-attributes-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-attr-values" data-type="attribute" style="display:none;">';
@@ -118,7 +118,7 @@
html += '</div>';
html += '<div class="filter-row filter-row-features" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label"><i class="icon-list-ul"></i> ' + (trans.features || 'Features') + ':</span>';
html += '<span class="filter-row-label">' + this.esIcon('list') + ' ' + (trans.features || 'Features') + ':</span>';
html += '<div class="filter-features-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-feat-values" data-type="feature" style="display:none;">';
@@ -129,19 +129,19 @@
html += '<div class="filter-row filter-row-entity-categories filter-row-multi" data-entity="categories" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -150,7 +150,7 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label"><i class="icon-sitemap"></i> ' + (trans.depth || 'Depth') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('account_tree') + ' ' + (trans.depth || 'Depth') + ':</span>';
html += '<select class="filter-depth-select">';
html += '<option value="">' + (trans.all_levels || 'All levels') + '</option>';
html += '<option value="1">' + (trans.level || 'Level') + ' 1 (' + (trans.root || 'Root') + ')</option>';
@@ -162,7 +162,7 @@
html += '<label class="filter-label"><input type="checkbox" class="filter-has-products"> ' + (trans.has_products || 'Has products') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-description"> ' + (trans.has_description || 'Has description') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-image"> ' + (trans.has_image || 'Has image') + '</label>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
@@ -170,19 +170,19 @@
html += '<div class="filter-row filter-row-entity-manufacturers filter-row-multi" data-entity="manufacturers" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -191,18 +191,18 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
@@ -210,19 +210,19 @@
html += '<div class="filter-row filter-row-entity-suppliers filter-row-multi" data-entity="suppliers" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -231,18 +231,18 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
@@ -250,19 +250,19 @@
html += '<div class="filter-row filter-row-entity-attributes filter-row-multi" data-entity="attributes" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -270,13 +270,13 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label"><i class="icon-tags"></i> ' + (trans.attribute_group || 'Group') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('label') + ' ' + (trans.attribute_group || 'Group') + ':</span>';
html += '<select class="filter-attribute-group-select">';
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-is-color"> ' + (trans.color_only || 'Color attributes') + '</label>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
@@ -284,19 +284,19 @@
html += '<div class="filter-row filter-row-entity-features filter-row-multi" data-entity="features" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
@@ -304,13 +304,13 @@
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label"><i class="icon-list-ul"></i> ' + (trans.feature_group || 'Group') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('list') + ' ' + (trans.feature_group || 'Group') + ':</span>';
html += '<select class="filter-feature-group-select">';
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-is-custom"> ' + (trans.custom_only || 'Custom values') + '</label>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
@@ -318,13 +318,13 @@
html += '<div class="filter-row filter-row-entity-cms" data-entity="cms" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-indexable"> ' + (trans.indexable || 'Indexable') + '</label>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
// Entity-specific filters: CMS Categories
html += '<div class="filter-row filter-row-entity-cms-categories" data-entity="cms_categories" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
// Entity-specific filters: Countries
@@ -333,12 +333,12 @@
html += '<label class="filter-label"><input type="checkbox" class="filter-has-holidays"> ' + (trans.has_holidays || 'Has holidays') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-contains-states"> ' + (trans.contains_states || 'Has states') + '</label>';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label"><i class="icon-globe"></i> ' + (trans.zone || 'Zone') + ':</span>';
html += '<span class="filter-select-label">' + this.esIcon('language') + ' ' + (trans.zone || 'Zone') + ':</span>';
html += '<select class="filter-zone-select">';
html += '<option value="">' + (trans.all_zones || 'All zones') + '</option>';
html += '</select>';
html += '</div>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></button>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>'; // End filter-panel
@@ -373,8 +373,8 @@
// Right side: action buttons
html += '<div class="dropdown-footer-right">';
html += '<button type="button" class="dropdown-action-btn btn-cancel"><i class="icon-times"></i> ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-save"><i class="icon-check"></i> ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-cancel">' + this.esIcon('close') + ' ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-save">' + this.esIcon('check') + ' ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
html += '</div>';
html += '</div>';

View File

@@ -107,9 +107,9 @@
!$(e.target).closest('.group-count-badge').length &&
!$(e.target).closest('.group-modifiers').length &&
!$(e.target).closest('.group-preview-badge').length &&
!$(e.target).closest('.toggle-count.clickable').length &&
!$(e.target).closest('.trait-total-count').length &&
!$(e.target).closest('.chip-preview-holidays').length &&
!$(e.target).closest('.chip-preview-btn').length) {
!$(e.target).closest('.chip-preview-holidays').length) {
self.hidePreviewPopover();
// Also close holiday popover
$('.holiday-preview-popover').remove();
@@ -139,7 +139,7 @@
this.$wrapper.on('click', '.btn-toggle-blocks', function(e) {
e.preventDefault();
var $blocksContent = self.$wrapper.find('.entity-selector-blocks-content');
var $icon = $(this).find('.material-icons');
var $icon = $(this).find('.es-icon');
$blocksContent.stop(true, true);
if ($blocksContent.is(':visible')) {
$blocksContent.slideUp(200);
@@ -152,29 +152,9 @@
}
});
// Filter chip/group preview eye button (unified handler)
this.$wrapper.on('click', '.chip-preview-btn', function(e) {
e.preventDefault();
e.stopPropagation();
var $btn = $(this);
if ($btn.hasClass('popover-open')) {
self.hidePreviewPopover();
} else {
var valueId = $btn.data('id');
var valueType = $btn.data('type');
var valueName = $btn.data('name');
var groupId = $btn.data('groupId');
if (valueId) {
// Value-level preview (specific attribute/feature value)
self.showFilterValuePreviewPopover($btn, valueId, valueType, valueName, groupId);
} else if (groupId) {
// Group-level preview (entire attribute/feature group)
self.showFilterGroupPreviewPopover($btn, groupId, valueType, valueName);
}
}
// Custom block input changes — update tab badge when value changes
this.$wrapper.on('input change', '.custom-block-content input, .custom-block-content textarea, .custom-block-content select', function() {
self.updateTabBadges();
});
// Group-level collapse toggle (click on group header or toggle icon)
@@ -190,23 +170,27 @@
});
// Toggle all groups (single button that switches between expand/collapse)
this.$wrapper.on('click', '.trait-header-actions .btn-toggle-groups', function(e) {
console.log('[ES-DEBUG] Binding .btn-toggle-groups click on wrapper:', self.$wrapper.attr('id'), 'found buttons:', self.$wrapper.find('.btn-toggle-groups').length);
this.$wrapper.on('click', '.btn-toggle-groups', function(e) {
e.preventDefault();
e.stopPropagation();
var $btn = $(this);
var currentState = $btn.attr('data-state') || 'collapsed';
var trans = self.config.trans || {};
console.log('[ES-DEBUG] .btn-toggle-groups CLICKED! currentState:', currentState, 'btn parent:', $btn.parent().attr('class'), 'groups found:', self.$wrapper.find('.selection-group').length);
if (currentState === 'collapsed') {
self.$wrapper.find('.selection-group').removeClass('collapsed');
$btn.attr('data-state', 'expanded');
$btn.attr('title', trans.collapse_all || 'Collapse all groups');
$btn.find('i').removeClass('icon-expand').addClass('icon-compress');
$btn.find('i').text('close_fullscreen');
console.log('[ES-DEBUG] Expanded all groups');
} else {
self.$wrapper.find('.selection-group').addClass('collapsed');
$btn.attr('data-state', 'collapsed');
$btn.attr('title', trans.expand_all || 'Expand all groups');
$btn.find('i').removeClass('icon-compress').addClass('icon-expand');
$btn.find('i').text('open_in_full');
console.log('[ES-DEBUG] Collapsed all groups');
}
});
@@ -526,8 +510,8 @@
var currentPattern = $tag.data('pattern');
var $editInput = $('<input type="text" class="pattern-tag-edit">').val(currentPattern);
var $saveBtn = $('<button type="button" class="btn-pattern-save" title="Save"><i class="icon-check"></i></button>');
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel"><i class="icon-times"></i></button>');
var $saveBtn = $('<button type="button" class="btn-pattern-save" title="Save">' + this.esIcon('check') + '</button>');
var $cancelBtn = $('<button type="button" class="btn-pattern-cancel" title="Cancel">' + this.esIcon('close') + '</button>');
var $editActions = $('<span class="pattern-edit-actions"></span>').append($saveBtn, $cancelBtn);
$tag.addClass('editing').find('.pattern-tag-text').hide();
@@ -576,50 +560,6 @@
$tag.removeClass('editing').find('.pattern-tag-text, .btn-remove-pattern').show();
});
// Handle mpr-info-wrapper tooltip with fixed positioning
this.$wrapper.on('mouseenter', '.mpr-info-wrapper[data-details]', function() {
var $wrapper = $(this);
if ($wrapper.data('tooltip-active')) return;
var content = $wrapper.attr('data-details');
var tooltipClass = $wrapper.attr('data-tooltip-class') || '';
var $tooltip = $('<div>', { class: 'mpr-tooltip mpr-tooltip-fixed ' + tooltipClass, html: content });
$('body').append($tooltip);
$wrapper.data('tooltip-active', true);
var offset = $wrapper.offset();
var triggerWidth = $wrapper.outerWidth();
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var left = offset.left + (triggerWidth / 2) - (tooltipWidth / 2);
var top = offset.top - tooltipHeight - 10;
if (left < 10) left = 10;
if (left + tooltipWidth > $(window).width() - 10) {
left = $(window).width() - tooltipWidth - 10;
}
$tooltip.css({
position: 'fixed',
left: left + 'px',
top: (top - $(window).scrollTop()) + 'px'
});
$wrapper.data('tooltip-el', $tooltip);
});
this.$wrapper.on('mouseleave', '.mpr-info-wrapper[data-details]', function() {
var $wrapper = $(this);
var $tooltip = $wrapper.data('tooltip-el');
if ($tooltip) {
$tooltip.remove();
}
$wrapper.data('tooltip-active', false);
$wrapper.data('tooltip-el', null);
});
// Handle numeric range input changes
this.$wrapper.on('change', '.range-min-input, .range-max-input', function() {
var $row = $(this).closest('.group-include, .exclude-row');
@@ -715,7 +655,7 @@
$chip.append($('<button>', {
type: 'button',
class: 'btn-remove-range',
html: '<i class="icon-times"></i>'
html: self.esIcon('close')
}));
$chipsContainer.append($chip);
@@ -871,11 +811,7 @@
$btn.attr('data-dir', newDir);
var $icon = $btn.find('i');
if (newDir === 'ASC') {
$icon.removeClass('icon-sort-amount-desc').addClass('icon-sort-amount-asc');
} else {
$icon.removeClass('icon-sort-amount-asc').addClass('icon-sort-amount-desc');
}
$icon.replaceWith(this.esIcon('sort'));
self.serializeAllBlocks();
self.refreshGroupPreviewIfOpen($group);
@@ -1056,7 +992,7 @@
if (isSelected) {
// Remove from pending selections
self.pendingSelections = self.pendingSelections.filter(function(s) {
return String(s.id) !== String(id);
return parseInt(s.id, 10) !== parseInt(id, 10);
});
self.removeSelection($picker, id);
$item.toggleClass('selected');
@@ -1070,9 +1006,10 @@
return;
}
var currentSelection = self.getCurrentSingleSelection();
var activeBlockType = self.activeGroup.blockType;
var currentSelection = self.getCurrentSingleSelection(activeBlockType);
if (currentSelection) {
var newEntityType = self.activeGroup.blockType;
var newEntityType = activeBlockType;
self.showReplaceConfirmation(currentSelection, { name: name, entityType: newEntityType }, function() {
// Add to pending selections
self.pendingSelections.push({ id: id, name: name, data: $item.data() });
@@ -1083,7 +1020,7 @@
} else {
// Add to pending selections
var exists = self.pendingSelections.some(function(s) {
return String(s.id) === String(id);
return parseInt(s.id, 10) === parseInt(id, 10);
});
if (!exists) {
self.pendingSelections.push({ id: id, name: name, data: $item.data() });
@@ -1108,7 +1045,7 @@
// Also remove from pending selections if dropdown is open
if (self.pendingSelections) {
self.pendingSelections = self.pendingSelections.filter(function(s) {
return String(s.id) !== String(id);
return parseInt(s.id, 10) !== parseInt(id, 10);
});
}
@@ -1235,7 +1172,7 @@
// Add to pending selections for Save button
var exists = self.pendingSelections.some(function(s) {
return String(s.id) === String(id);
return parseInt(s.id, 10) === parseInt(id, 10);
});
if (!exists) {
self.pendingSelections.push({
@@ -1369,7 +1306,7 @@
var currentDir = $btn.data('dir');
var newDir = currentDir === 'ASC' ? 'DESC' : 'ASC';
$btn.data('dir', newDir);
$btn.find('i').attr('class', newDir === 'ASC' ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc');
$btn.find('i').replaceWith(this.esIcon('sort_by_alpha'));
self.currentSort.dir = newDir;
self.refreshSearch();
});
@@ -1383,8 +1320,7 @@
$item.toggleClass('collapsed');
var isCollapsed = $item.hasClass('collapsed');
$(this).find('i').toggleClass('icon-caret-down', !isCollapsed)
.toggleClass('icon-caret-right', isCollapsed);
$(this).find('i').text(isCollapsed ? 'arrow_right' : 'arrow_drop_down');
var descendants = self.findTreeDescendants($item, $allItems);
for (var i = 0; i < descendants.length; i++) {
@@ -1515,7 +1451,7 @@
$child.removeClass('selected');
}
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.attr('title', trans.select_with_children || 'Select with all children');
} else {
var section = self.activeGroup.section;
@@ -1553,7 +1489,7 @@
self.showValidationError(skipMsg);
}
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
}
@@ -1574,7 +1510,7 @@
this.$dropdown.on('click', '.category-tree .btn-expand-all', function(e) {
e.preventDefault();
self.$dropdown.find('.tree-item').removeClass('collapsed').show();
self.$dropdown.find('.tree-toggle i').removeClass('icon-caret-right').addClass('icon-caret-down');
self.$dropdown.find('.tree-toggle i').replaceWith(this.esIcon('arrow_drop_down'));
});
// Tree view: Collapse all
@@ -1594,7 +1530,7 @@
if (level === minLevel) {
if (hasChildren) {
$item.addClass('collapsed');
$item.find('.tree-toggle i').removeClass('icon-caret-down').addClass('icon-caret-right');
$item.find('.tree-toggle i').replaceWith(self.esIcon('arrow_right'));
}
$item.show();
} else {
@@ -1850,6 +1786,10 @@
// Toggle filter group - show values
this.$dropdown.on('click', '.filter-group-toggle', function(e) {
// Ignore clicks on the preview badge
if ($(e.target).closest('.toggle-count.clickable').length) {
return;
}
e.preventDefault();
var $btn = $(this);
var groupId = $btn.data('group-id');
@@ -1866,26 +1806,20 @@
}
});
// Filter preview eye button (dropdown-level, since dropdown is appended to body)
this.$dropdown.on('click', '.chip-preview-btn', function(e) {
e.preventDefault();
// Filter group toggle count badge click for preview popover
this.$dropdown.on('click', '.filter-group-toggle .toggle-count.clickable', function(e) {
e.stopPropagation();
e.preventDefault();
var $btn = $(this);
var $badge = $(this);
var groupId = $badge.data('groupId');
var groupType = $badge.data('type');
var groupName = $badge.data('groupName');
if ($btn.hasClass('popover-open')) {
if ($badge.hasClass('popover-open')) {
self.hidePreviewPopover();
} else {
var valueId = $btn.data('id');
var valueType = $btn.data('type');
var valueName = $btn.data('name');
var groupId = $btn.data('groupId');
if (valueId) {
self.showFilterValuePreviewPopover($btn, valueId, valueType, valueName, groupId);
} else if (groupId) {
self.showFilterGroupPreviewPopover($btn, groupId, valueType, valueName);
}
self.showFilterGroupPreviewPopover($badge, groupId, groupType, groupName);
}
});
@@ -1994,104 +1928,6 @@
}
});
// Tooltip hover events
this.$wrapper.on('mouseenter', '.mpr-info-wrapper:not(.pinned)', function() {
var $wrapper = $(this);
var content = $wrapper.attr('data-tooltip');
if (!content) return;
// Don't show hover tooltip if another is pinned
if ($('.mpr-tooltip-fixed.pinned').length) return;
// Remove any existing non-pinned tooltip
$('.mpr-tooltip-fixed:not(.pinned)').remove();
// Create tooltip
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed' }).html(content);
$('body').append($tooltip);
// Use getBoundingClientRect for viewport-relative positioning (fixed)
var rect = $wrapper[0].getBoundingClientRect();
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
var top = rect.top - tooltipHeight - 8;
// Keep tooltip within viewport
if (left < 10) left = 10;
if (left + tooltipWidth > window.innerWidth - 10) {
left = window.innerWidth - tooltipWidth - 10;
}
if (top < 10) {
top = rect.bottom + 8;
}
$tooltip.css({ top: top, left: left });
});
this.$wrapper.on('mouseleave', '.mpr-info-wrapper:not(.pinned)', function() {
$('.mpr-tooltip-fixed:not(.pinned)').remove();
});
// Click to pin tooltip
this.$wrapper.on('click', '.mpr-info-wrapper', function(e) {
e.preventDefault();
e.stopPropagation();
var $wrapper = $(this);
// If already pinned, unpin and close
if ($wrapper.hasClass('pinned')) {
$wrapper.removeClass('pinned');
$wrapper.find('.material-icons').text('info');
$('.mpr-tooltip-fixed.pinned').remove();
return;
}
// Close any other pinned tooltips
$('.mpr-info-wrapper.pinned').removeClass('pinned').find('.material-icons').text('info');
$('.mpr-tooltip-fixed').remove();
var content = $wrapper.attr('data-tooltip');
if (!content) return;
// Pin this one
$wrapper.addClass('pinned');
$wrapper.find('.material-icons').text('close');
// Create pinned tooltip with close button
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed pinned' });
var $closeBtn = $('<button>', { class: 'mpr-tooltip-close', type: 'button' })
.append($('<i>', { class: 'material-icons', text: 'close' }));
$tooltip.append($closeBtn).append(content);
$('body').append($tooltip);
// Close button click
$closeBtn.on('click', function() {
$wrapper.removeClass('pinned');
$wrapper.find('.material-icons').text('info');
$tooltip.remove();
});
// Position
var rect = $wrapper[0].getBoundingClientRect();
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
var top = rect.top - tooltipHeight - 8;
if (left < 10) left = 10;
if (left + tooltipWidth > window.innerWidth - 10) {
left = window.innerWidth - tooltipWidth - 10;
}
if (top < 10) {
top = rect.bottom + 8;
}
$tooltip.css({ top: top, left: left });
});
}
};

View File

@@ -228,7 +228,6 @@
if (!this.$dropdown || !this.filterableData) return;
var self = this;
var previewLabel = self.config.trans && self.config.trans.preview || 'Preview';
// Render attribute group toggle buttons
var $attrContainer = this.$dropdown.find('.filter-attributes-container');
@@ -236,17 +235,12 @@
if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
this.filterableData.attributes.forEach(function(group) {
var html = '<span class="filter-chip-wrapper">';
html += '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) {
html += '<span class="toggle-count">(' + group.count + ')</span>';
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + group.count + '</span>';
}
html += '</button>';
html += '<button type="button" class="chip-preview-btn" data-group-id="' + group.id + '" data-type="attribute" data-name="' + self.escapeAttr(group.name) + '" title="' + previewLabel + '">';
html += '<i class="icon-eye"></i>';
html += '</button>';
html += '</span>';
$attrContainer.append(html);
});
this.$dropdown.find('.filter-row-attributes').show();
@@ -258,17 +252,12 @@
if (this.filterableData.features && this.filterableData.features.length > 0) {
this.filterableData.features.forEach(function(group) {
var html = '<span class="filter-chip-wrapper">';
html += '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) {
html += '<span class="toggle-count">(' + group.count + ')</span>';
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + group.count + '</span>';
}
html += '</button>';
html += '<button type="button" class="chip-preview-btn" data-group-id="' + group.id + '" data-type="feature" data-name="' + self.escapeAttr(group.name) + '" title="' + previewLabel + '">';
html += '<i class="icon-eye"></i>';
html += '</button>';
html += '</span>';
$featContainer.append(html);
});
this.$dropdown.find('.filter-row-features').show();
@@ -306,7 +295,6 @@
var colorStyle = val.color ? ' style="--chip-color: ' + val.color + '"' : '';
var colorClass = val.color ? ' has-color' : '';
html += '<span class="filter-chip-wrapper">';
html += '<button type="button" class="filter-chip ' + chipClass + activeClass + colorClass + '" data-id="' + val.id + '" data-group-id="' + groupId + '"' + colorStyle + '>';
if (val.color) {
html += '<span class="chip-color-dot"></span>';
@@ -316,17 +304,13 @@
html += '<span class="chip-count">(' + val.count + ')</span>';
}
html += '</button>';
html += '<button type="button" class="chip-preview-btn" data-id="' + val.id + '" data-group-id="' + groupId + '" data-type="' + type + '" data-name="' + self.escapeAttr(val.name) + '" title="' + (self.config.trans && self.config.trans.preview || 'Preview') + '">';
html += '<i class="icon-eye"></i>';
html += '</button>';
html += '</span>';
});
$valuesContainer.html(html);
// Add close button as sibling (outside filter-values-container, inside filter-row-values)
$filterRowValues.find('.btn-close-values').remove();
$filterRowValues.append('<button type="button" class="btn-close-values"><i class="icon-times"></i></button>');
$filterRowValues.append('<button type="button" class="btn-close-values">' + this.esIcon('close') + '</button>');
$filterRowValues.show();
// Scroll into view if needed

View File

@@ -47,13 +47,13 @@
// Group header
html += '<div class="group-header">';
html += '<span class="group-collapse-toggle"><i class="icon-chevron-up"></i></span>';
html += '<span class="group-collapse-toggle">' + this.esIcon('expand_less') + '</span>';
html += '<span class="group-name-wrapper">';
html += '<input type="text" class="group-name-input" value="" placeholder="' + defaultGroupName + '" title="' + (trans.click_to_name || 'Click to name this group') + '">';
html += '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
html += '<span class="group-count-badge" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '</span>';
html += '<button type="button" class="btn-remove-group" title="' + (trans.remove_group || 'Remove group') + '">';
html += '<i class="icon-trash"></i>';
html += this.esIcon('delete');
html += '</button>';
html += '</div>';
@@ -65,16 +65,16 @@
html += '<div class="section-row">';
html += '<div class="method-selector-wrapper">';
html += '<select class="include-method-select">' + methodOptions + '</select>';
html += '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
html += '<span class="condition-match-count no-matches">' + this.esIcon('visibility') + ' <span class="preview-count">0</span></span>';
html += '<span class="method-info-placeholder"></span>';
html += '</div>';
var noItemsText = trans.no_items_selected || 'No items selected - use search below';
html += '<div class="value-picker include-picker" style="display:none;" data-search-entity="' + blockType + '">';
html += '<div class="entity-chips include-chips" data-placeholder="' + noItemsText + '"></div>';
html += '<div class="entity-search-box">';
html += '<i class="icon-search entity-search-icon"></i>';
html += this.esIcon('search', 'entity-search-icon');
html += '<input type="text" class="entity-search-input" placeholder="' + (trans.search_placeholder || 'Search by name, reference, ID...') + '" autocomplete="off">';
html += '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '</div>';
html += '<input type="hidden" class="include-values-data" value="[]">';
html += '</div>';
@@ -84,7 +84,7 @@
// Excludes section (collapsed by default)
html += '<div class="group-excludes">';
html += '<button type="button" class="btn-add-exclude">';
html += '<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions');
html += this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions');
html += '</button>';
html += '</div>';
@@ -92,11 +92,11 @@
html += '<div class="group-modifiers">';
html += '<span class="modifier-inline modifier-limit">';
html += '<span class="modifier-label">' + (trans.limit || 'Limit') + '</span>';
html += '<input type="number" class="group-modifier-limit" placeholder="" min="1" step="1" title="' + (trans.limit_tooltip || 'Max items to return (empty = all)') + '">';
html += '<input type="number" class="group-modifier-limit mpr-input-compact" placeholder="" min="1" step="1" title="' + (trans.limit_tooltip || 'Max items to return (empty = all)') + '">';
html += '</span>';
html += '<span class="modifier-inline modifier-sort">';
html += '<span class="modifier-label">' + (trans.sort || 'Sort') + '</span>';
html += '<select class="group-modifier-sort">';
html += '<select class="group-modifier-sort mpr-input-compact">';
html += '<option value="sales" selected>' + (trans.sort_bestsellers || 'Best sellers') + '</option>';
html += '<option value="date_add">' + (trans.sort_newest || 'Newest') + '</option>';
html += '<option value="price">' + (trans.sort_price || 'Price') + '</option>';
@@ -106,11 +106,11 @@
html += '<option value="random">' + (trans.sort_random || 'Random') + '</option>';
html += '</select>';
html += '<button type="button" class="btn-sort-dir" data-dir="DESC" title="' + (trans.sort_direction || 'Sort direction') + '">';
html += '<i class="icon-sort-amount-desc"></i>';
html += this.esIcon('sort');
html += '</button>';
html += '</span>';
html += '<span class="group-preview-badge clickable" title="' + (trans.preview_results || 'Preview results') + '">';
html += '<i class="icon-eye"></i> <span class="preview-count"></span>';
html += this.esIcon('visibility') + ' <span class="preview-count"></span>';
html += '</span>';
html += '</div>';
@@ -213,12 +213,32 @@
if (groupCount > 0) {
// Show loading state first
if ($badge.length) {
$badge.addClass('loading').html('<i class="icon-spinner icon-spin"></i>');
$badge.addClass('loading').html(self.esIcon('progress_activity', 'es-spin'));
} else {
$tab.append('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
$tab.append('<span class="tab-badge loading">' + self.esIcon('progress_activity', 'es-spin') + '</span>');
}
$tab.addClass('has-data');
blockTypesWithData.push(blockType);
} else if ($block.hasClass('custom-block')) {
// Custom blocks: check if any input/textarea/select has a non-empty value
var hasCustomValue = false;
$block.find('.custom-block-content').find('input, textarea, select').each(function() {
if ($(this).val() && $(this).val().trim() !== '') {
hasCustomValue = true;
return false;
}
});
if (hasCustomValue) {
if ($badge.length) {
$badge.removeClass('loading').html(self.esIcon('check'));
} else {
$tab.append('<span class="tab-badge">' + self.esIcon('check') + '</span>');
}
$tab.addClass('has-data');
} else {
$badge.remove();
$tab.removeClass('has-data');
}
} else {
$badge.remove();
$tab.removeClass('has-data');
@@ -312,7 +332,7 @@
var $badge = $tab.find('.tab-badge');
if ($badge.length) {
$badge.removeClass('loading').html('<i class="icon-eye"></i> ' + count);
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + count);
// Store preview data for later popover use
$tab.data('previewData', { count: count, success: true });
}
@@ -377,10 +397,10 @@
// Show loading state
var $badge = $tab.find('.tab-badge');
if (!$badge.length) {
$badge = $('<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>');
$badge = $('<span class="tab-badge loading">' + this.esIcon('progress_activity', 'es-spin') + '</span>');
$tab.append($badge);
} else {
$badge.addClass('loading').html('<i class="icon-spinner icon-spin"></i>');
$badge.addClass('loading').html(this.esIcon('progress_activity', 'es-spin'));
}
$tab.addClass('has-data');
@@ -401,7 +421,7 @@
success: function(response) {
if (response.success) {
var $badge = $tab.find('.tab-badge');
$badge.removeClass('loading').html('<i class="icon-eye"></i> ' + response.count);
$badge.removeClass('loading').html(self.esIcon('visibility') + ' ' + response.count);
// Store preview data for popover
$tab.data('previewData', response);
@@ -979,7 +999,7 @@
}
// Show loading spinner
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches').show();
// Store condition data on badge for popover
@@ -1063,7 +1083,7 @@
// Special case: "All countries" method - fetch holidays for all countries
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
console.log('[updateConditionCount] All countries method - fetching all country holidays');
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches country-holidays').show();
// First fetch all active country IDs, then get holidays
@@ -1170,7 +1190,7 @@
var isCountrySelection = (searchEntity === 'countries' && valueType === 'entity_search');
console.log('[updateConditionCount] isCountrySelection:', isCountrySelection, 'values:', values);
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.find('.preview-count').html(this.esIcon('progress_activity', 'es-spin'));
$countEl.removeClass('clickable no-matches country-holidays').show();
// For countries, fetch holiday count
@@ -1283,7 +1303,7 @@
}
// Show loading
$badge.html('<i class="icon-spinner icon-spin"></i>').show();
$badge.html(this.esIcon('progress_activity', 'es-spin')).show();
$.ajax({
url: this.config.ajaxUrl,
@@ -1302,7 +1322,7 @@
var excludeCount = response.exclude_count || 0;
// Update badge with eye icon and count
var badgeHtml = '<i class="icon-eye"></i> ' + finalCount;
var badgeHtml = self.esIcon('visibility') + ' ' + finalCount;
if (excludeCount > 0) {
badgeHtml += ' <span class="exclude-info">(-' + excludeCount + ')</span>';
}
@@ -1343,7 +1363,7 @@
// Build the full excludes structure with first row
var html = '<div class="except-separator">';
html += '<span class="except-label"><i class="icon-ban"></i> ' + (trans.except || 'EXCEPT') + '</span>';
html += '<span class="except-label">' + this.esIcon('block') + ' ' + (trans.except || 'EXCEPT') + '</span>';
html += '</div>';
html += '<div class="exclude-rows-container">';
@@ -1351,7 +1371,7 @@
html += '</div>';
html += '<button type="button" class="btn-add-another-exclude">';
html += '<i class="icon-plus"></i> ' + (trans.add_another_exception || 'Add another exception');
html += this.esIcon('add') + ' ' + (trans.add_another_exception || 'Add another exception');
html += '</button>';
$excludesDiv.addClass('has-excludes').html(html);
@@ -1422,11 +1442,11 @@
html += '<div class="exclude-header-row">';
html += '<div class="method-selector-wrapper">';
html += '<select class="exclude-method-select">' + excludeMethodOptions + '</select>';
html += '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
html += '<span class="condition-match-count no-matches">' + this.esIcon('visibility') + ' <span class="preview-count">0</span></span>';
html += '<span class="method-info-placeholder"></span>';
html += '</div>';
html += '<button type="button" class="btn-remove-exclude-row" title="' + (trans.remove_this_exception || 'Remove this exception') + '">';
html += '<i class="icon-trash"></i>';
html += this.esIcon('delete');
html += '</button>';
html += '</div>';
@@ -1452,7 +1472,7 @@
var $excludesDiv = $group.find('.group-excludes');
$excludesDiv.removeClass('has-excludes').html(
'<button type="button" class="btn-add-exclude">' +
'<i class="icon-plus"></i> ' + (trans.add_exceptions || 'Add exceptions') +
this.esIcon('add') + ' ' + (trans.add_exceptions || 'Add exceptions') +
'</button>'
);
// Unlock the method selector since no excludes exist
@@ -1558,9 +1578,9 @@
var noItemsText = trans.no_items_selected || 'No items selected - use search below';
html += '<div class="entity-chips ' + chipsClass + '" data-placeholder="' + this.escapeAttr(noItemsText) + '"></div>';
html += '<div class="entity-search-box">';
html += '<i class="icon-search entity-search-icon"></i>';
html += this.esIcon('search', 'entity-search-icon');
html += '<input type="text" class="entity-search-input" placeholder="' + this.escapeAttr(trans.search_placeholder || 'Search by name, reference, ID...') + '" autocomplete="off">';
html += '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
html += '<span class="search-loading" style="display:none;">' + this.esIcon('progress_activity', 'es-spin') + '</span>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
break;
@@ -1587,11 +1607,11 @@
html += '<div class="pattern-tag draft-tag" data-case-sensitive="0">';
html += '<button type="button" class="btn-toggle-case" title="' + this.escapeAttr(trans.case_insensitive || 'Case insensitive - click to toggle') + '"><span class="case-icon">aa</span></button>';
html += '<input type="text" class="pattern-input" value="" placeholder="' + this.escapeAttr(trans.enter_pattern || 'e.g. *cotton*') + '">';
html += '<span class="pattern-match-count" title="' + this.escapeAttr(trans.click_to_preview || 'Click to preview matches') + '"><i class="icon-eye"></i> <span class="count-value"></span></span>';
html += '<button type="button" class="btn-add-pattern" title="' + this.escapeAttr(trans.add_pattern || 'Add pattern (Enter)') + '"><i class="icon-plus"></i></button>';
html += '<span class="pattern-match-count" title="' + this.escapeAttr(trans.click_to_preview || 'Click to preview matches') + '">' + this.esIcon('visibility') + ' <span class="count-value"></span></span>';
html += '<button type="button" class="btn-add-pattern" title="' + this.escapeAttr(trans.add_pattern || 'Add pattern (Enter)') + '">' + this.esIcon('add') + '</button>';
html += '</div>';
html += '<span class="mpr-info-wrapper" data-details="' + this.escapeAttr(tooltipContent) + '">';
html += '<span class="mpr-icon icon-info link"></span>';
html += this.esIcon('info');
html += '</span>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
@@ -1613,7 +1633,7 @@
html += '<input type="number" class="range-min-input" value="" placeholder="' + this.escapeAttr(trans.min || 'Min') + '" step="0.01">';
html += '<span class="range-separator">-</span>';
html += '<input type="number" class="range-max-input" value="" placeholder="' + this.escapeAttr(trans.max || 'Max') + '" step="0.01">';
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '"><i class="icon-plus"></i></button>';
html += '<button type="button" class="btn-add-range" title="' + this.escapeAttr(trans.add_range || 'Add range') + '">' + this.esIcon('add') + '</button>';
html += '</div>';
html += '</div>';
html += '<input type="hidden" class="' + dataClass + '" value="[]">';
@@ -1681,7 +1701,7 @@
html += '</div>';
}
html += '<div class="combination-groups-container">';
html += '<span class="combination-loading"><i class="icon-spinner icon-spin"></i> ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
html += '<span class="combination-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' + this.escapeHtml(trans.loading || 'Loading...') + '</span>';
html += '</div>';
html += '</div>';
// Store mode along with attributes: { mode: 'products'|'combinations', attributes: { groupId: [valueIds] } }
@@ -1727,28 +1747,14 @@
}
},
getSortIconClass: function(sortBy, sortDir) {
var isAsc = (sortDir === 'ASC');
getSortIconName: function(sortBy, sortDir) {
switch (sortBy) {
case 'name':
return isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
case 'price':
case 'quantity':
case 'product_count':
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'date_add':
case 'newest_products':
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
case 'sales':
case 'total_sales':
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
case 'position':
return isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
return 'sort_by_alpha';
case 'random':
return 'icon-random';
return 'shuffle';
default:
return isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
return 'sort';
}
},
@@ -1793,7 +1799,7 @@
$btn.attr('data-sort', newSort);
$btn.attr('data-dir', newDir);
$btn.attr('title', newLabel + ' ' + (newDir === 'DESC' ? '↓' : '↑'));
$btn.find('i').attr('class', this.getSortIconClass(newSort, newDir));
$btn.find('i').replaceWith(this.esIcon(this.getSortIconName(newSort, newDir)));
},
// Validation
@@ -1833,7 +1839,7 @@
// Add error message after header
var $error = $('<div>', {
class: 'trait-validation-error',
html: '<i class="icon-warning"></i> ' + message
html: this.esIcon('warning') + ' ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
@@ -1852,8 +1858,7 @@
clearValidationError: function() {
this.$wrapper.removeClass('has-validation-error');
this.$wrapper.find('.trait-validation-error').remove();
},
}
};
})(jQuery);

View File

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

View File

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

View File

@@ -40,11 +40,6 @@
sort_dir: this.currentSort ? this.currentSort.dir : 'ASC'
};
// Add product_selection_level if set
if (this.config.productSelectionLevel && this.config.productSelectionLevel !== 'product') {
requestData.product_selection_level = this.config.productSelectionLevel;
}
// Add refine query if present
if (this.refineQuery) {
requestData.refine = this.refineQuery;
@@ -366,14 +361,12 @@
var html = '';
if (visibleResults.length === 0 && !appendMode) {
html = '<div class="no-results"><i class="icon-search"></i> ' + (trans.no_results || 'No results found') + '</div>';
html = '<div class="no-results">' + this.esIcon('search') + ' ' + (trans.no_results || 'No results found') + '</div>';
} else {
visibleResults.forEach(function(item) {
var isSelected = selectedIds.indexOf(String(item.id)) !== -1;
var itemClass = 'dropdown-item' + (isSelected ? ' selected' : '');
if (item.type === 'product') itemClass += ' result-item-product';
if (item.is_combination) itemClass += ' is-combination';
if (item.is_parent) itemClass += ' is-parent-product';
html += '<div class="' + itemClass + '" ';
html += 'data-id="' + self.escapeAttr(item.id) + '" ';
@@ -381,30 +374,29 @@
if (item.image) html += ' data-image="' + self.escapeAttr(item.image) + '"';
if (item.subtitle) html += ' data-subtitle="' + self.escapeAttr(item.subtitle) + '"';
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
if (item.attributes) html += ' data-attributes="' + self.escapeAttr(item.attributes) + '"';
html += '>';
html += '<span class="result-checkbox"><i class="icon-check"></i></span>';
html += '<span class="result-checkbox">' + self.esIcon('check') + '</span>';
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : null;
// Countries show flags
if (searchEntity === 'countries' && item.iso_code) {
var flagUrl = 'https://flagcdn.com/w40/' + item.iso_code.toLowerCase() + '.png';
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;"><i class="icon-flag"></i></span></div>';
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;">' + self.esIcon('flag') + '</span></div>';
} else if (item.image) {
html += '<div class="result-image"><img src="' + self.escapeAttr(item.image) + '" alt=""></div>';
} else {
// Entity-specific icons
var iconClass = 'icon-cube'; // default
if (searchEntity === 'categories') iconClass = 'icon-folder';
else if (searchEntity === 'manufacturers') iconClass = 'icon-building';
else if (searchEntity === 'suppliers') iconClass = 'icon-truck';
else if (searchEntity === 'attributes') iconClass = 'icon-paint-brush';
else if (searchEntity === 'features') iconClass = 'icon-list-ul';
else if (searchEntity === 'cms') iconClass = 'icon-file-text-o';
else if (searchEntity === 'cms_categories') iconClass = 'icon-folder-o';
html += '<div class="result-icon"><i class="' + iconClass + '"></i></div>';
var iconName = 'widgets'; // default
if (searchEntity === 'categories') iconName = 'folder';
else if (searchEntity === 'manufacturers') iconName = 'business';
else if (searchEntity === 'suppliers') iconName = 'local_shipping';
else if (searchEntity === 'attributes') iconName = 'brush';
else if (searchEntity === 'features') iconName = 'list';
else if (searchEntity === 'cms') iconName = 'description';
else if (searchEntity === 'cms_categories') iconName = 'folder';
html += '<div class="result-icon">' + self.esIcon(iconName) + '</div>';
}
html += '<div class="result-info">';
@@ -596,10 +588,10 @@
for (var i = 0; i < history.length; i++) {
var query = history[i];
html += '<div class="history-item" data-query="' + this.escapeAttr(query) + '">';
html += '<i class="icon-clock-o"></i>';
html += this.esIcon('schedule');
html += '<span class="history-query">' + this.escapeHtml(query) + '</span>';
html += '<button type="button" class="btn-delete-history" title="' + (trans.remove || 'Remove') + '">';
html += '<i class="icon-times"></i>';
html += this.esIcon('close');
html += '</button>';
html += '</div>';
}

View File

@@ -36,7 +36,7 @@
var searchEntity = this.activeGroup ? this.activeGroup.searchEntity : 'categories';
// Show loading
$results.html('<div class="tree-loading"><i class="icon-spinner icon-spin"></i> ' +
$results.html('<div class="tree-loading">' + this.esIcon('progress_activity', 'es-spin') + ' ' +
this.escapeHtml(trans.loading || 'Loading...') + '</div>');
// Fetch tree data
@@ -115,11 +115,11 @@
html += '<div class="tree-toolbar">';
html += '<button type="button" class="btn-expand-all" title="' +
this.escapeAttr(trans.expand_all || 'Expand all') + '">';
html += '<i class="icon-plus-square-o"></i> ' + this.escapeHtml(trans.expand_all || 'Expand all');
html += this.esIcon('add_box') + ' ' + this.escapeHtml(trans.expand_all || 'Expand all');
html += '</button>';
html += '<button type="button" class="btn-collapse-all" title="' +
this.escapeAttr(trans.collapse_all || 'Collapse all') + '">';
html += '<i class="icon-minus-square-o"></i> ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
html += this.esIcon('indeterminate_check_box') + ' ' + this.escapeHtml(trans.collapse_all || 'Collapse all');
html += '</button>';
html += '</div>';
@@ -178,21 +178,21 @@
// Toggle button (expand/collapse)
if (hasChildren) {
html += '<span class="tree-toggle"><i class="icon-caret-down"></i></span>';
html += '<span class="tree-toggle">' + self.esIcon('arrow_drop_down') + '</span>';
// Select with children button (next to toggle on the left)
html += '<button type="button" class="btn-select-children" title="' +
self.escapeAttr(trans.select_with_children || 'Select with all children') + '">';
html += '<i class="icon-check-square-o"></i>';
html += self.esIcon('check_box');
html += '</button>';
} else {
html += '<span class="tree-toggle tree-leaf"></span>';
}
// Checkbox indicator
html += '<span class="tree-checkbox"><i class="icon-check"></i></span>';
html += '<span class="tree-checkbox">' + self.esIcon('check') + '</span>';
// Category icon
html += '<span class="tree-icon"><i class="icon-folder"></i></span>';
html += '<span class="tree-icon">' + self.esIcon('folder') + '</span>';
// Name
html += '<span class="tree-name">' + self.escapeHtml(node.name) + '</span>';
@@ -203,7 +203,7 @@
var countLabel = node.page_count ? (trans.pages || 'pages') : (trans.products || 'products');
html += '<span class="tree-count clickable" data-category-id="' + node.id + '" ';
html += 'title="' + self.escapeAttr(itemCount + ' ' + countLabel) + '">';
html += '<i class="icon-eye"></i> ' + itemCount;
html += self.esIcon('visibility') + ' ' + itemCount;
html += '</span>';
}
@@ -346,10 +346,10 @@
});
if (isParentSelected && allChildrenSelected) {
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
$btn.find('i').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
} else {
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.attr('title', trans.select_with_children || 'Select with all children');
}
});

View File

@@ -19,6 +19,97 @@
// Create mixin namespace
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
// ---------------------------------------------------------------
// Icon framework detection & FA4 mapping (module-level singleton)
// ---------------------------------------------------------------
var _iconMode = null;
/**
* Material Icons → FontAwesome 4 class mapping.
* FA4 uses class-based icons (icon-name), Material uses text content.
*/
var FA4_MAP = {
'account_tree': 'icon-sitemap',
'add': 'icon-plus',
'add_box': 'icon-plus-square',
'arrow_downward': 'icon-sort-desc',
'arrow_drop_down': 'icon-caret-down',
'arrow_right': 'icon-chevron-right',
'arrow_upward': 'icon-sort-asc',
'block': 'icon-ban',
'brush': 'icon-paint-brush',
'business': 'icon-building',
'check': 'icon-check',
'check_box': 'icon-check-square',
'check_box_outline_blank': 'icon-square-o',
'check_circle': 'icon-check-circle',
'close': 'icon-times',
'delete': 'icon-trash',
'description': 'icon-file-text',
'error': 'icon-exclamation-circle',
'event': 'icon-calendar',
'event_busy': 'icon-calendar-times-o',
'expand_less': 'icon-chevron-up',
'expand_more': 'icon-chevron-down',
'filter_list': 'icon-filter',
'flag': 'icon-flag',
'folder': 'icon-folder',
'folder_open': 'icon-folder-open',
'indeterminate_check_box': 'icon-minus-square',
'info': 'icon-info-circle',
'inventory_2': 'icon-archive',
'label': 'icon-tag',
'language': 'icon-globe',
'lightbulb': 'icon-lightbulb-o',
'list': 'icon-list',
'list_alt': 'icon-list-alt',
'local_shipping': 'icon-truck',
'lock': 'icon-lock',
'my_location': 'icon-crosshairs',
'open_in_full': 'icon-expand',
'payments': 'icon-credit-card',
'progress_activity': 'icon-circle-o-notch',
'schedule': 'icon-clock-o',
'search': 'icon-search',
'shopping_cart': 'icon-shopping-cart',
'shuffle': 'icon-random',
'sort': 'icon-sort',
'sort_by_alpha': 'icon-sort-alpha-asc',
'star': 'icon-star',
'sync': 'icon-refresh',
'tune': 'icon-sliders',
'visibility': 'icon-eye',
'warning': 'icon-warning',
'widgets': 'icon-th-large'
};
/**
* Detect icon framework: 'material' (PS 8+/9+) or 'fa4' (PS 1.6/1.7).
* Checks PHP-set data attribute first, falls back to font detection.
*/
function detectIconMode() {
if (_iconMode !== null) return _iconMode;
// 1. PHP sets data-icon-mode on the wrapper
var $w = $('.entity-selector-trait[data-icon-mode], .target-conditions-trait[data-icon-mode]').first();
if ($w.length && $w.data('icon-mode')) {
_iconMode = $w.data('icon-mode');
return _iconMode;
}
// 2. Fallback: probe whether Material Icons font is loaded
var test = document.createElement('i');
test.className = 'material-icons';
test.style.cssText = 'position:absolute;left:-9999px;top:-9999px;font-size:16px;pointer-events:none';
test.textContent = 'check';
(document.body || document.documentElement).appendChild(test);
var family = (window.getComputedStyle(test).fontFamily || '').toLowerCase();
test.parentNode.removeChild(test);
_iconMode = (family.indexOf('material') !== -1) ? 'material' : 'fa4';
return _iconMode;
}
// Utility functions mixin
window._EntitySelectorMixins.utils = {
@@ -60,18 +151,62 @@
.replace(/'/g, '&#039;');
},
/**
* Icon helper — returns HTML for an icon that works on PS 1.6 through 9.x.
* Automatically uses Material Icons (PS 8+/9+) or FontAwesome 4 (PS 1.6/1.7).
*
* @param {string} name - Canonical icon name (Material Icons name, e.g. 'lock', 'search', 'delete')
* @param {string} [extraClass] - Additional CSS class(es) (e.g. 'es-spin', 'method-trigger-icon')
* @returns {string} HTML string for an <i> element
*/
esIcon: function(name, extraClass) {
var mode = detectIconMode();
if (mode === 'material') {
var cls = 'material-icons es-icon';
if (extraClass) cls += ' ' + extraClass;
return '<i class="' + cls + '">' + name + '</i>';
}
// FA4: icon is encoded in the class name, no text content
var mapped = FA4_MAP[name] || 'icon-circle';
var cls = mapped + ' es-icon';
if (extraClass) cls += ' ' + extraClass;
return '<i class="' + cls + '"></i>';
},
/**
* Update an existing <i> icon element to show a different icon.
* Handles both Material Icons and FA4 modes.
*
* @param {jQuery} $el - The <i> element to update
* @param {string} name - Canonical icon name
* @param {string} [extraClass] - Additional CSS class(es) to preserve
*/
esIconUpdate: function($el, name, extraClass) {
var mode = detectIconMode();
if (mode === 'material') {
var cls = 'material-icons es-icon';
if (extraClass) cls += ' ' + extraClass;
$el.attr('class', cls).text(name);
} else {
var mapped = FA4_MAP[name] || 'icon-circle';
var cls = mapped + ' es-icon';
if (extraClass) cls += ' ' + extraClass;
$el.attr('class', cls).text('');
}
},
getEntityTypeIcon: function(entityType) {
var icons = {
'products': 'icon-shopping-cart',
'categories': 'icon-folder-open',
'manufacturers': 'icon-building',
'suppliers': 'icon-truck',
'attributes': 'icon-list-alt',
'features': 'icon-tags',
'cms': 'icon-file-text',
'cms_categories': 'icon-folder'
'products': 'shopping_cart',
'categories': 'folder_open',
'manufacturers': 'business',
'suppliers': 'local_shipping',
'attributes': 'list_alt',
'features': 'label',
'cms': 'description',
'cms_categories': 'folder'
};
return icons[entityType] || 'icon-cube';
return icons[entityType] || 'widgets';
},
getEntityTypeLabel: function(entityType) {
@@ -116,7 +251,7 @@
this.$wrapper.find('.trait-validation-error').remove();
var $error = $('<div>', {
class: 'trait-validation-error',
html: '<i class="icon-warning"></i> ' + message
html: this.esIcon('warning') + ' ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
$('html, body').animate({ scrollTop: this.$wrapper.offset().top - 100 }, 300);

View File

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

View File

@@ -332,7 +332,7 @@
color: darken($es-primary, 10%);
}
i.material-icons {
i {
font-size: 14px;
}
}
@@ -831,7 +831,7 @@
color: $es-text-secondary;
}
i.material-icons {
i {
font-size: 18px;
}
}
@@ -853,7 +853,7 @@
color: $es-text-muted;
font-size: $es-font-size-sm;
i.material-icons {
i {
font-size: 20px;
}
@@ -868,7 +868,7 @@
padding: $es-spacing-xl 0;
color: $es-text-muted;
i.material-icons {
i {
font-size: 48px;
opacity: 0.4;
margin-bottom: $es-spacing-sm;
@@ -978,7 +978,7 @@
border-bottom: 1px solid $es-border-color;
background: $es-slate-50;
i.material-icons {
i {
font-size: 18px;
color: $es-text-muted;
}

View File

@@ -261,7 +261,7 @@
color: $es-primary;
}
.material-icons {
> i {
font-size: 20px !important;
}
}

View File

@@ -579,7 +579,6 @@
}
// Icon styles
> .material-icons,
> i:first-child {
flex-shrink: 0;
width: 16px;
@@ -639,7 +638,7 @@
opacity: 0.8;
}
.material-icons {
> i {
font-size: 12px;
line-height: 1;
}

View File

@@ -24,8 +24,8 @@
}
}
// Modal container
.mpr-modal {
// Modal container (exclude Bootstrap .modal to prevent collision)
.mpr-modal:not(.modal) {
position: fixed;
top: 50%;
left: 50%;
@@ -337,7 +337,7 @@
color: $es-text-primary;
margin: 0;
i.material-icons {
i> i {
font-size: 20px;
color: $es-primary;
}
@@ -392,7 +392,7 @@
padding: $es-spacing-xl 0;
color: $es-text-muted;
i.material-icons {
i> i {
font-size: 48px;
opacity: 0.5;
margin-bottom: $es-spacing-md;

View File

@@ -329,7 +329,7 @@
background: $es-slate-200;
}
.material-icons {
> i {
color: $es-slate-400;
font-size: 20px;
}
@@ -357,7 +357,7 @@
border-radius: $es-radius-full;
white-space: nowrap;
.material-icons {
> i {
font-size: 14px;
opacity: 0.7;
}

View File

@@ -17,13 +17,13 @@
vertical-align: middle;
margin-left: 0.25rem;
.material-icons {
font-size: 16px;
> i {
font-size: 14px;
color: $es-text-muted;
transition: color 0.15s ease;
}
&:hover .material-icons {
&:hover > i {
color: $es-primary;
}
}
@@ -92,7 +92,7 @@
line-height: 1;
transition: background-color 0.15s ease;
.material-icons {
> i {
font-size: 16px;
color: $es-text-muted;
}
@@ -100,7 +100,7 @@
&:hover {
background: $es-slate-100;
.material-icons {
> i {
color: $es-slate-700;
}
}

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,307 @@
/**
* Entity Selector - Core Module
* Factory, initialization, state management
* @partial _core.js
*
* IMPORTANT: This file must be loaded LAST in the concatenation order
* as it combines all mixins from other partials.
*
* EXTRACTION SOURCE: assets/js/admin/entity-selector.js
* Lines: 15-55 (createTargetConditionsInstance, state variables)
* 56-110 (init method)
* 108-132 (observeNewSelects)
* 7889-7951 (Factory object, window export, document ready)
*
* Contains:
* - createTargetConditionsInstance() - Factory function
* - State variable initialization
* - init() - Main initialization method
* - observeNewSelects() - MutationObserver for dynamic selects
* - loadExistingSelections() - Restore saved state
* - TargetConditions factory object
* - window.TargetConditions export
* - Document ready auto-initialization
*/
(function($) {
'use strict';
/**
* Create a new TargetConditions instance
* Each instance is independent and manages its own wrapper/state
*/
function createTargetConditionsInstance() {
// Base instance object with state variables
var instance = {
config: {},
$wrapper: null,
$dropdown: null,
activeGroup: null, // { blockType, groupIndex, section: 'include'|'exclude' }
searchTimeout: null,
searchResults: [],
searchTotal: 0,
searchOffset: 0,
searchQuery: '',
isLoading: false,
loadMoreCount: 20,
// Sort, filter, view state
viewMode: 'list',
currentSort: { field: 'name', dir: 'ASC' },
refineQuery: '',
refineNegate: false,
filters: {
inStock: false,
discounted: false,
priceMin: null,
priceMax: null,
attributes: [],
features: [],
// Entity-specific filters
productCountMin: null,
productCountMax: null,
salesMin: null,
salesMax: null,
turnoverMin: null,
turnoverMax: null,
depth: null,
hasProducts: false,
hasDescription: false,
hasImage: false,
activeOnly: true, // Default to active only
attributeGroup: null,
featureGroup: null,
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
},
filterableData: null,
// Search history
searchHistory: {},
searchHistoryMax: 10,
searchHistoryKey: 'targetConditionsSearchHistory',
// Chips visibility
maxVisibleChips: 20,
// Method dropdown references
$methodDropdownMenu: null,
$methodDropdownSelect: null,
$methodDropdownTrigger: null,
// Preview state
$previewPopover: null,
$activeBadge: null,
$previewList: null,
previewLoadedCount: 0,
previewBlockType: null,
allPreviewData: null,
// Count update timeout
countUpdateTimeout: null,
init: function(options) {
this.config = $.extend({
id: 'target-conditions',
name: 'target_conditions',
namePrefix: 'target_',
mode: 'multi', // Global mode: 'multi' or 'single'
blocks: {},
ajaxUrl: '',
trans: {}
}, options);
this.$wrapper = $('[data-entity-selector-id="' + this.config.id + '"]');
if (!this.$wrapper.length) {
return;
}
// Global single mode - hide "Add Group" buttons
if (this.config.mode === 'single') {
this.$wrapper.find('.btn-add-group').hide();
this.$wrapper.find('.group-excludes').hide();
this.$wrapper.find('.group-modifiers').hide();
}
// Add fullwidth class to parent form-group (skip for form-group layout)
var hasLayoutFormGroup = this.$wrapper.hasClass('layout-form-group');
var $entitySelectorFormGroup = this.$wrapper.closest('.entity-selector-form-group');
if (!hasLayoutFormGroup && !$entitySelectorFormGroup.length) {
var $formGroup = this.$wrapper.closest('.form-group');
$formGroup.addClass('condition-trait-fullwidth');
$formGroup.find('.col-lg-offset-3').removeClass('col-lg-offset-3');
}
this.createDropdown();
this.bindEvents();
this.loadExistingSelections();
this.loadSearchHistory();
// Initialize styled method dropdowns
this.initMethodDropdowns();
// Watch for dynamically added selects
this.observeNewSelects();
// Update counts on page load
var self = this;
setTimeout(function() {
self.updateTabBadges();
self.updateAllConditionCounts();
}, 100);
},
observeNewSelects: function() {
var self = this;
if (typeof MutationObserver === 'undefined') {
return;
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
$(mutation.addedNodes).find('.include-method-select, .exclude-method-select').each(function() {
self.enhanceMethodSelect($(this));
});
}
});
});
observer.observe(this.$wrapper[0], {
childList: true,
subtree: true
});
},
loadExistingSelections: function() {
// TODO: Extract full implementation from original
// Reads JSON from hidden input and populates chips
}
};
// Merge all mixins into the instance
// Each mixin adds its methods to window._EntitySelectorMixins
var mixins = window._EntitySelectorMixins || {};
// Merge utils mixin
if (mixins.utils) {
$.extend(instance, mixins.utils);
}
// Merge events mixin
if (mixins.events) {
$.extend(instance, mixins.events);
}
// Merge dropdown mixin
if (mixins.dropdown) {
$.extend(instance, mixins.dropdown);
}
// Merge search mixin
if (mixins.search) {
$.extend(instance, mixins.search);
}
// Merge filters mixin
if (mixins.filters) {
$.extend(instance, mixins.filters);
}
// Merge chips mixin
if (mixins.chips) {
$.extend(instance, mixins.chips);
}
// Merge groups mixin
if (mixins.groups) {
$.extend(instance, mixins.groups);
}
// Merge methods mixin
if (mixins.methods) {
$.extend(instance, mixins.methods);
}
// Merge preview mixin
if (mixins.preview) {
$.extend(instance, mixins.preview);
}
// Merge tree mixin
if (mixins.tree) {
$.extend(instance, mixins.tree);
}
// Merge validation mixin
if (mixins.validation) {
$.extend(instance, mixins.validation);
}
return instance;
}
// Factory object for creating and managing instances
var TargetConditions = {
instances: [],
// Create and initialize a new instance
create: function(options) {
var instance = createTargetConditionsInstance();
instance.init(options);
this.instances.push(instance);
return instance;
},
// For backwards compatibility - init creates a new instance
init: function(options) {
return this.create(options);
},
// Validate all instances - returns true if all valid
validateAll: function() {
var allValid = true;
for (var i = 0; i < this.instances.length; i++) {
if (!this.instances[i].validate()) {
allValid = false;
}
}
return allValid;
}
};
// Export to window
window.TargetConditions = TargetConditions;
// Auto-initialize on document ready
$(document).ready(function() {
// Auto-initialize from data-config attributes on wrapper elements
$('[data-entity-selector-id]').each(function() {
var configData = $(this).data('config');
if (configData) {
TargetConditions.create(configData);
}
});
// Tips box toggle handler
$(document).on('click', '.target-tips-box .tips-header', function(e) {
e.preventDefault();
$(this).closest('.target-tips-box').toggleClass('expanded');
});
// Form submission validation for required target conditions
$(document).on('submit', 'form', function(e) {
var $form = $(this);
if ($form.find('.target-conditions-trait[data-required]').length > 0) {
if (!TargetConditions.validateAll()) {
e.preventDefault();
return false;
}
}
});
});
})(jQuery);

View File

@@ -0,0 +1,438 @@
/**
* Entity Selector - Dropdown Module
* Search dropdown UI creation and positioning
* @partial _dropdown.js
*/
(function($) {
'use strict';
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
window._EntitySelectorMixins.dropdown = {
createDropdown: function() {
this.$wrapper.find('.target-search-dropdown').remove();
var trans = this.config.trans || {};
var html = '<div class="target-search-dropdown view-list">';
// Header with results count, actions, sort controls, view mode
html += '<div class="dropdown-header">';
html += '<span class="results-count">0 results</span>';
html += '<div class="dropdown-actions">';
// Select all / Clear buttons with keyboard shortcuts
html += '<button type="button" class="btn-select-all" title="' + (trans.select_all || 'Select all visible') + '">';
html += this.esIcon('check_box') + ' ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += '</button>';
html += '<button type="button" class="btn-clear-selection" title="' + (trans.clear_selection || 'Clear selection') + '">';
html += this.esIcon('check_box_outline_blank') + ' ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += '</button>';
// Sort controls - options with data-entities attribute for entity-specific filtering
html += '<div class="sort-controls">';
html += '<select class="sort-field-select" title="Sort by">';
// Universal options (all entities)
html += '<option value="name">' + (trans.sort_name || 'Name') + '</option>';
html += '<option value="id">' + (trans.sort_id || 'ID') + '</option>';
html += '<option value="selected">' + (trans.sort_selected || 'Selected') + '</option>';
// Product-specific
html += '<option value="price" data-entities="products">' + (trans.sort_price || 'Price') + '</option>';
html += '<option value="stock" data-entities="products">' + (trans.sort_stock || 'Stock') + '</option>';
html += '<option value="popularity" data-entities="products">' + (trans.sort_popularity || 'Sales') + '</option>';
html += '<option value="reference" data-entities="products">' + (trans.sort_reference || 'Reference') + '</option>';
// Position-based entities (categories, cms, cms_categories, attributes)
html += '<option value="position" data-entities="categories,cms,cms_categories,attributes">' + (trans.sort_position || 'Position') + '</option>';
// Product count (categories, manufacturers, suppliers)
html += '<option value="product_count" data-entities="categories,manufacturers,suppliers">' + (trans.sort_product_count || 'Products') + '</option>';
html += '</select>';
html += '<button type="button" class="btn-sort-dir" data-dir="ASC" title="Sort direction">';
html += this.esIcon('sort_by_alpha');
html += '</button>';
// 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">' + (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>';
html += '<option value="cols-5">5 ' + (trans.cols || 'cols') + '</option>';
html += '<option value="cols-6">6 ' + (trans.cols || 'cols') + '</option>';
html += '<option value="cols-7">7 ' + (trans.cols || 'cols') + '</option>';
html += '<option value="cols-8">8 ' + (trans.cols || 'cols') + '</option>';
html += '</select>';
html += '</div>'; // End sort-controls
// Refine search
html += '<div class="refine-compact">';
html += '<button type="button" class="btn-refine-negate" title="' + (trans.exclude_matches || 'Exclude matches (NOT contains)') + '">' + this.esIcon('block') + '</button>';
html += '<input type="text" class="refine-input" placeholder="' + (trans.refine_short || 'Refine...') + '">';
html += '<button type="button" class="btn-clear-refine" style="display:none;">' + this.esIcon('close') + '</button>';
html += '</div>';
// Filter toggle button
html += '<button type="button" class="btn-toggle-filters" title="' + (trans.toggle_filters || 'Filters') + '">';
html += this.esIcon('filter_list');
html += '</button>';
// History button
html += '<button type="button" class="btn-show-history" title="' + (trans.recent_searches || 'Recent searches') + '">';
html += this.esIcon('schedule');
html += '</button>';
html += '</div>'; // End dropdown-actions
html += '</div>'; // End dropdown-header
// Filter panel
html += '<div class="filter-panel">';
// Quick filters row (for products)
html += '<div class="filter-row filter-row-quick" data-entity="products">';
html += '<label class="filter-label"><input type="checkbox" class="filter-in-stock"> ' + (trans.in_stock || 'In stock') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-discounted"> ' + (trans.discounted || 'On sale') + '</label>';
// Price range
html += '<div class="filter-price-range">';
html += '<span class="filter-price-label">' + (trans.price || 'Price') + ':</span>';
html += '<input type="number" class="filter-price-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="0.01">';
html += '<span class="filter-price-sep">-</span>';
html += '<input type="number" class="filter-price-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="0.01">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters" title="' + (trans.clear_filters || 'Clear filters') + '">';
html += this.esIcon('close');
html += '</button>';
html += '</div>';
// Attribute/Feature filter toggles for products
html += '<div class="filter-row filter-row-attributes" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label">' + this.esIcon('label') + ' ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<div class="filter-attributes-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-attr-values" data-type="attribute" style="display:none;">';
html += '<div class="filter-values-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-features" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label">' + this.esIcon('list') + ' ' + (trans.features || 'Features') + ':</span>';
html += '<div class="filter-features-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-feat-values" data-type="feature" style="display:none;">';
html += '<div class="filter-values-container"></div>';
html += '</div>';
// Entity-specific filters: Categories
html += '<div class="filter-row filter-row-entity-categories filter-row-multi" data-entity="categories" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('account_tree') + ' ' + (trans.depth || 'Depth') + ':</span>';
html += '<select class="filter-depth-select">';
html += '<option value="">' + (trans.all_levels || 'All levels') + '</option>';
html += '<option value="1">' + (trans.level || 'Level') + ' 1 (' + (trans.root || 'Root') + ')</option>';
html += '<option value="2">' + (trans.level || 'Level') + ' 2</option>';
html += '<option value="3">' + (trans.level || 'Level') + ' 3</option>';
html += '<option value="4">' + (trans.level || 'Level') + ' 4+</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-products"> ' + (trans.has_products || 'Has products') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-description"> ' + (trans.has_description || 'Has description') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-image"> ' + (trans.has_image || 'Has image') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
// Entity-specific filters: Manufacturers
html += '<div class="filter-row filter-row-entity-manufacturers filter-row-multi" data-entity="manufacturers" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
// Entity-specific filters: Suppliers
html += '<div class="filter-row filter-row-entity-suppliers filter-row-multi" data-entity="suppliers" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('event') + ' ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label">' + this.esIcon('schedule') + ' ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
// Entity-specific filters: Attributes
html += '<div class="filter-row filter-row-entity-attributes filter-row-multi" data-entity="attributes" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('label') + ' ' + (trans.attribute_group || 'Group') + ':</span>';
html += '<select class="filter-attribute-group-select">';
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-is-color"> ' + (trans.color_only || 'Color attributes') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
// Entity-specific filters: Features
html += '<div class="filter-row filter-row-entity-features filter-row-multi" data-entity="features" style="display:none;">';
html += '<div class="filter-subrow">';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('inventory_2') + ' ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('shopping_cart') + ' ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label">' + this.esIcon('payments') + ' ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '</div>';
html += '<div class="filter-subrow">';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('list') + ' ' + (trans.feature_group || 'Group') + ':</span>';
html += '<select class="filter-feature-group-select">';
html += '<option value="">' + (trans.all_groups || 'All groups') + '</option>';
html += '</select>';
html += '</div>';
html += '<label class="filter-label"><input type="checkbox" class="filter-is-custom"> ' + (trans.custom_only || 'Custom values') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>';
// Entity-specific filters: CMS Pages
html += '<div class="filter-row filter-row-entity-cms" data-entity="cms" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-indexable"> ' + (trans.indexable || 'Indexable') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
// Entity-specific filters: CMS Categories
html += '<div class="filter-row filter-row-entity-cms-categories" data-entity="cms_categories" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
// Entity-specific filters: Countries
html += '<div class="filter-row filter-row-entity-countries" data-entity="countries" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-has-holidays"> ' + (trans.has_holidays || 'Has holidays') + '</label>';
html += '<label class="filter-label"><input type="checkbox" class="filter-contains-states"> ' + (trans.contains_states || 'Has states') + '</label>';
html += '<div class="filter-select-group">';
html += '<span class="filter-select-label">' + this.esIcon('language') + ' ' + (trans.zone || 'Zone') + ':</span>';
html += '<select class="filter-zone-select">';
html += '<option value="">' + (trans.all_zones || 'All zones') + '</option>';
html += '</select>';
html += '</div>';
html += '<button type="button" class="btn-clear-filters">' + this.esIcon('close') + '</button>';
html += '</div>';
html += '</div>'; // End filter-panel
// Results header for list view (product columns)
html += '<div class="results-header">';
html += '<span class="header-spacer"></span>';
html += '<span class="header-col header-col-name">' + (trans.product || 'Product') + '</span>';
html += '<span class="header-col header-col-price">' + (trans.price || 'Price') + '</span>';
html += '<span class="header-col header-col-sale">' + (trans.sale || 'Sale') + '</span>';
html += '<span class="header-col header-col-stock">' + (trans.stock || 'Stock') + '</span>';
html += '<span class="header-col header-col-sales">' + (trans.sold || 'Sold') + '</span>';
html += '</div>';
// Results
html += '<div class="dropdown-results"></div>';
// Footer - unified load more + actions
html += '<div class="dropdown-footer">';
// Left side: load more
html += '<div class="dropdown-footer-left" style="visibility:hidden;">';
html += '<span class="load-label">' + (trans.load || 'Load') + '</span>';
html += '<select class="load-more-select">';
html += '<option value="20">20</option>';
html += '<option value="50">50</option>';
html += '<option value="100">100</option>';
html += '<option value="all">' + (trans.all || 'All') + '</option>';
html += '</select>';
html += '<span class="remaining-text">' + (trans.of || 'of') + ' <strong class="remaining-count">0</strong> ' + (trans.remaining || 'remaining') + '</span>';
html += '</div>';
// Right side: action buttons
html += '<div class="dropdown-footer-right">';
html += '<button type="button" class="dropdown-action-btn btn-cancel">' + this.esIcon('close') + ' ' + (trans.cancel || 'Cancel') + ' <span class="btn-shortcut">Esc</span></button>';
html += '<button type="button" class="dropdown-action-btn btn-save">' + this.esIcon('check') + ' ' + (trans.save || 'Save') + ' <span class="btn-shortcut">⏎</span></button>';
html += '</div>';
html += '</div>';
html += '</div>';
this.$dropdown = $(html);
$('body').append(this.$dropdown);
},
hideDropdown: function() {
if (this.$dropdown) {
this.$dropdown.removeClass('show');
}
this.activeGroup = null;
},
positionDropdown: function($input) {
if (!this.$dropdown) return;
var $picker = $input.closest('.value-picker');
var $searchBox = $input.closest('.entity-search-box');
// Get absolute positions (dropdown is appended to body)
var searchBoxOffset = $searchBox.offset();
var searchBoxHeight = $searchBox.outerHeight();
var pickerOffset = $picker.offset();
var pickerWidth = $picker.outerWidth();
// Calculate position relative to document
var dropdownTop = searchBoxOffset.top + searchBoxHeight + 4;
var dropdownLeft = pickerOffset.left;
var dropdownWidth = Math.max(pickerWidth, 400);
// Ensure dropdown doesn't overflow the viewport horizontally
var viewportWidth = $(window).width();
if (dropdownLeft + dropdownWidth > viewportWidth - 10) {
dropdownWidth = viewportWidth - dropdownLeft - 10;
}
// Ensure dropdown doesn't overflow viewport vertically
var viewportHeight = $(window).height();
var scrollTop = $(window).scrollTop();
var maxHeight = viewportHeight - (dropdownTop - scrollTop) - 20;
maxHeight = Math.max(maxHeight, 400);
this.$dropdown.css({
position: 'absolute',
top: dropdownTop,
left: dropdownLeft,
width: dropdownWidth,
maxHeight: maxHeight,
zIndex: 10000
});
// Show the dropdown
this.$dropdown.addClass('show');
}
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,397 @@
/**
* Entity Selector - Filters Module
* Filter panel, filter state management
* @partial _filters.js
*
* EXTRACTION SOURCE: assets/js/admin/entity-selector.js
* Lines: 6605-6758 (filter methods)
*
* Contains:
* - clearFilters() - Reset all filters
* - resetFiltersWithoutSearch() - Reset without triggering search
* - updateFilterPanelForEntity() - Show/hide filters based on entity type
* - loadFilterableData() - Load attributes/features for filter panel
* - renderFilterDropdowns() - Render attribute/feature group toggles
* - showFilterGroupValues() - Show values for a filter group
* - hideFilterGroupValues() - Hide filter values row
* - updateFilterToggleStates() - Update toggle states based on selections
*/
(function($) {
'use strict';
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
window._EntitySelectorMixins.filters = {
clearFilters: function() {
this.refineQuery = '';
this.refineNegate = false;
this.filters = {
inStock: false,
discounted: false,
priceMin: null,
priceMax: null,
attributes: [],
features: [],
productCountMin: null,
productCountMax: null,
salesMin: null,
salesMax: null,
turnoverMin: null,
turnoverMax: null,
depth: null,
hasProducts: false,
hasDescription: false,
hasImage: false,
activeOnly: true,
attributeGroup: null,
featureGroup: null,
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
};
if (this.$dropdown) {
var trans = this.config.trans || {};
this.$dropdown.find('.refine-input').val('');
this.$dropdown.find('.btn-refine-negate').removeClass('active');
this.$dropdown.find('.filter-in-stock').prop('checked', false);
this.$dropdown.find('.filter-discounted').prop('checked', false);
this.$dropdown.find('.filter-price-min, .filter-price-max').val('');
this.$dropdown.find('.filter-attr-chip, .filter-feat-chip').removeClass('active');
this.$dropdown.find('.filter-product-count-min, .filter-product-count-max').val('');
this.$dropdown.find('.filter-sales-min, .filter-sales-max').val('');
this.$dropdown.find('.filter-depth-select').val('');
this.$dropdown.find('.filter-has-products').prop('checked', false);
this.$dropdown.find('.filter-active-only').prop('checked', true);
// Country filters
this.$dropdown.find('.filter-has-holidays').prop('checked', false);
this.$dropdown.find('.filter-contains-states').prop('checked', false);
this.$dropdown.find('.filter-zone-select').val('');
}
this.refreshSearch();
},
resetFiltersWithoutSearch: function() {
// Same as clearFilters but doesn't trigger search
// Used when switching entity types
this.refineQuery = '';
this.refineNegate = false;
this.filters = {
inStock: false,
discounted: false,
priceMin: null,
priceMax: null,
attributes: [],
features: [],
productCountMin: null,
productCountMax: null,
salesMin: null,
salesMax: null,
turnoverMin: null,
turnoverMax: null,
depth: null,
hasProducts: false,
hasDescription: false,
hasImage: false,
activeOnly: true,
attributeGroup: null,
featureGroup: null,
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
};
},
updateFilterPanelForEntity: function(entityType) {
if (!this.$dropdown) {
return;
}
var $panel = this.$dropdown.find('.filter-panel');
// Hide all entity-specific filter rows
$panel.find('.filter-row').hide();
// Show filters for current entity type
$panel.find('.filter-row[data-entity="' + entityType + '"]').show();
$panel.find('.filter-row-entity-' + entityType.replace('_', '-')).show();
// Show/hide tree view option based on entity type
var isCategory = (entityType === 'categories' || entityType === 'cms_categories');
this.$dropdown.find('.tree-view-option').toggle(isCategory);
// Default to tree view for categories (only if currently on list mode)
if (isCategory && this.viewMode === 'list') {
this.viewMode = 'tree';
this.$dropdown.find('.view-mode-select').val('tree');
this.$dropdown.removeClass('view-list view-cols-2 view-cols-3 view-cols-4 view-cols-5 view-cols-6 view-cols-7 view-cols-8').addClass('view-tree');
} else if (!isCategory && this.viewMode === 'tree') {
// If switching away from categories while in tree mode, switch to list
this.viewMode = 'list';
this.$dropdown.find('.view-mode-select').val('list');
this.$dropdown.removeClass('view-tree view-cols-2 view-cols-3 view-cols-4 view-cols-5 view-cols-6 view-cols-7 view-cols-8').addClass('view-list');
}
// Load zones for countries filter
if (entityType === 'countries') {
this.loadZonesForCountryFilter();
}
// Update sort options for entity type
this.updateSortOptionsForEntity(entityType);
},
/**
* Show/hide sort options based on entity type
* Options with data-entities attribute are only shown for matching entities
*/
updateSortOptionsForEntity: function(entityType) {
if (!this.$dropdown) {
return;
}
var $select = this.$dropdown.find('.sort-field-select');
var currentValue = $select.val();
var hasCurrentOption = false;
$select.find('option').each(function() {
var $option = $(this);
var entities = $option.data('entities');
// Options without data-entities are universal (always shown)
if (!entities) {
$option.show();
if ($option.val() === currentValue) {
hasCurrentOption = true;
}
return;
}
// Check if this entity type is in the allowed list
var allowedEntities = entities.split(',');
var isAllowed = allowedEntities.indexOf(entityType) !== -1;
$option.toggle(isAllowed);
if (isAllowed && $option.val() === currentValue) {
hasCurrentOption = true;
}
});
// If current sort field is not available for this entity, reset to 'name'
if (!hasCurrentOption) {
$select.val('name');
this.currentSort.field = 'name';
}
},
loadFilterableData: function() {
var self = this;
if (this.filterableData) {
this.renderFilterDropdowns();
return;
}
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
data: {
ajax: 1,
action: 'getTargetFilterableAttributes',
trait: 'EntitySelector'
},
dataType: 'json',
success: function(response) {
if (response.success && response.data) {
self.filterableData = response.data;
self.renderFilterDropdowns();
}
}
});
},
renderFilterDropdowns: function() {
if (!this.$dropdown || !this.filterableData) return;
var self = this;
// Render attribute group toggle buttons
var $attrContainer = this.$dropdown.find('.filter-attributes-container');
$attrContainer.empty();
if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
this.filterableData.attributes.forEach(function(group) {
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) {
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="attribute" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + group.count + '</span>';
}
html += '</button>';
$attrContainer.append(html);
});
this.$dropdown.find('.filter-row-attributes').show();
}
// Render feature group toggle buttons
var $featContainer = this.$dropdown.find('.filter-features-container');
$featContainer.empty();
if (this.filterableData.features && this.filterableData.features.length > 0) {
this.filterableData.features.forEach(function(group) {
var html = '<button type="button" class="filter-group-toggle" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">';
html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) {
html += '<span class="toggle-count clickable" data-group-id="' + group.id + '" data-type="feature" data-group-name="' + self.escapeAttr(group.name) + '">' + self.esIcon('visibility') + ' ' + group.count + '</span>';
}
html += '</button>';
$featContainer.append(html);
});
this.$dropdown.find('.filter-row-features').show();
}
},
showFilterGroupValues: function(groupId, type) {
if (!this.filterableData) return;
var self = this;
var groups = type === 'attribute' ? this.filterableData.attributes : this.filterableData.features;
var group = groups.find(function(g) { return g.id == groupId; });
if (!group) return;
// Hide all values rows first, then show the correct one
this.$dropdown.find('.filter-row-values').hide();
// Target the correct values row based on type
var valuesRowClass = type === 'attribute' ? '.filter-row-attr-values' : '.filter-row-feat-values';
var $filterRowValues = this.$dropdown.find(valuesRowClass);
var $valuesContainer = $filterRowValues.find('.filter-values-container');
$valuesContainer.empty();
// Add group label
var html = '<span class="filter-values-label">' + group.name + ':</span>';
// Add chips
group.values.forEach(function(val) {
var isActive = type === 'attribute'
? self.filters.attributes.indexOf(val.id) !== -1
: self.filters.features.indexOf(val.id) !== -1;
var activeClass = isActive ? ' active' : '';
var chipClass = type === 'attribute' ? 'filter-attr-chip' : 'filter-feat-chip';
var colorStyle = val.color ? ' style="--chip-color: ' + val.color + '"' : '';
var colorClass = val.color ? ' has-color' : '';
html += '<button type="button" class="filter-chip ' + chipClass + activeClass + colorClass + '" data-id="' + val.id + '" data-group-id="' + groupId + '"' + colorStyle + '>';
if (val.color) {
html += '<span class="chip-color-dot"></span>';
}
html += '<span class="chip-name">' + val.name + '</span>';
if (val.count !== undefined) {
html += '<span class="chip-count">(' + val.count + ')</span>';
}
html += '</button>';
});
$valuesContainer.html(html);
// Add close button as sibling (outside filter-values-container, inside filter-row-values)
$filterRowValues.find('.btn-close-values').remove();
$filterRowValues.append('<button type="button" class="btn-close-values">' + this.esIcon('close') + '</button>');
$filterRowValues.show();
// Scroll into view if needed
var rowValues = $filterRowValues[0];
if (rowValues) {
rowValues.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
},
hideFilterGroupValues: function() {
this.$dropdown.find('.filter-row-values').hide();
this.$dropdown.find('.filter-group-toggle').removeClass('active');
},
updateFilterToggleStates: function() {
if (!this.$dropdown || !this.filterableData) return;
var self = this;
// Update attribute group toggles
if (this.filterableData.attributes) {
this.filterableData.attributes.forEach(function(group) {
var $toggle = self.$dropdown.find('.filter-group-toggle[data-group-id="' + group.id + '"][data-type="attribute"]');
var hasActiveInGroup = group.values.some(function(val) {
return self.filters.attributes.indexOf(val.id) !== -1;
});
$toggle.toggleClass('has-selection', hasActiveInGroup);
});
}
// Update feature group toggles
if (this.filterableData.features) {
this.filterableData.features.forEach(function(group) {
var $toggle = self.$dropdown.find('.filter-group-toggle[data-group-id="' + group.id + '"][data-type="feature"]');
var hasActiveInGroup = group.values.some(function(val) {
return self.filters.features.indexOf(val.id) !== -1;
});
$toggle.toggleClass('has-selection', hasActiveInGroup);
});
}
},
/**
* Load zones for country filter dropdown
*/
loadZonesForCountryFilter: function() {
var self = this;
if (this.zonesLoaded || !this.$dropdown) {
return;
}
var $select = this.$dropdown.find('.filter-zone-select');
if (!$select.length) {
return;
}
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getZonesForFilter',
trait: 'EntitySelector'
},
success: function(response) {
if (response.success && response.zones && response.zones.length > 0) {
var trans = self.config.trans || {};
$select.empty();
$select.append('<option value="">' + (trans.all_zones || 'All zones') + '</option>');
response.zones.forEach(function(zone) {
$select.append('<option value="' + zone.id + '">' + self.escapeHtml(zone.name) + '</option>');
});
self.zonesLoaded = true;
}
}
});
}
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,878 @@
/**
* Entity Selector - Methods Module
* Method dropdown rendering, value pickers, combination picker
* @partial _methods.js
*
* EXTRACTION SOURCE: assets/js/admin/entity-selector.js
* Lines: 6760-6848 (initMethodDropdowns, enhanceMethodSelect)
* 6849-7051 (showMethodDropdownMenu, buildMethodDropdownMenuHtml, closeMethodDropdownMenu)
* 7053-7138 (populateTiles, applyRangeInputConstraints, showRangeInputError)
* 7139-7380 (combination picker methods)
* 7382-7550 (updateMethodInfoPlaceholder, getBuiltInMethodHelp)
* 7748-7888 (buildSortOptions, updateModifierButtonState, updateMethodSelectorLock)
*
* Contains:
* - initMethodDropdowns() - Initialize styled dropdowns
* - enhanceMethodSelect() - Convert select to styled dropdown
* - showMethodDropdownMenu() - Show method selection menu
* - buildMethodDropdownMenuHtml() - Build menu HTML
* - closeMethodDropdownMenu() - Close dropdown menu
* - updateMethodTrigger() - Update trigger display
* - populateTiles() - Build multi-select tiles
* - applyRangeInputConstraints() - Set numeric input constraints
* - showRangeInputError() - Display validation error
* - loadCombinationAttributeGroups() - Load attribute groups for picker
* - loadCombinationAttributeValues() - Load values for attribute group
* - restoreCombinationSelections() - Restore saved combination state
* - updateCombinationData() - Save combination selection
* - updateCombinationGroupCounts() - Update selection counts
* - updateMethodInfoPlaceholder() - Show method help
* - getBuiltInMethodHelp() - Get help text for methods
* - buildSortOptions() - Build sort dropdown options
* - updateModifierButtonState() - Update modifier toggle state
* - updateMethodSelectorLock() - Lock/unlock method selector
*/
(function($) {
'use strict';
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
window._EntitySelectorMixins.methods = {
/**
* Initialize styled method dropdowns for all method selects
*/
initMethodDropdowns: function() {
var self = this;
this.$wrapper.find('.include-method-select').each(function() {
self.enhanceMethodSelect($(this));
});
this.$wrapper.find('.exclude-method-select').each(function() {
self.enhanceMethodSelect($(this));
});
this.initMethodInfoPlaceholders();
},
/**
* Initialize info placeholders for all existing method selects
*/
initMethodInfoPlaceholders: function() {
var self = this;
this.$wrapper.find('.selection-group').each(function() {
var $group = $(this);
var $block = $group.closest('.target-block');
var blockType = $block.data('blockType') || 'products';
// Include method info
var includeMethod = $group.find('.include-method-select').val() || 'all';
self.updateMethodInfoPlaceholder($group.find('.method-selector-wrapper'), includeMethod, blockType);
// Exclude methods info
$group.find('.exclude-row').each(function() {
var $row = $(this);
var excludeMethod = $row.find('.exclude-method-select').val();
if (excludeMethod) {
self.updateMethodInfoPlaceholder($row.find('.method-selector-wrapper'), excludeMethod, blockType);
}
});
});
},
/**
* Enhance a single method select with styled dropdown
*/
enhanceMethodSelect: function($select) {
var self = this;
if (!$select.length || $select.data('methodDropdownInit')) {
return;
}
$select.data('methodDropdownInit', true);
$select.addClass('method-select-hidden');
var $selectedOption = $select.find('option:selected');
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
var selectedLabel = $selectedOption.text();
var triggerHtml = '<div class="method-dropdown-trigger">';
triggerHtml += this.esIcon(selectedIcon, 'method-trigger-icon');
triggerHtml += '<span class="method-trigger-label">' + this.escapeHtml(selectedLabel) + '</span>';
triggerHtml += this.esIcon('arrow_drop_down', 'method-trigger-caret');
triggerHtml += '</div>';
var $trigger = $(triggerHtml);
$select.after($trigger);
$trigger.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var $wrapper = $select.closest('.method-selector-wrapper');
if ($wrapper.hasClass('selector-locked')) {
return;
}
self.showMethodDropdownMenu($select, $trigger);
});
$select.on('change.methodDropdown', function() {
self.updateMethodTrigger($select, $trigger);
});
},
/**
* Update the trigger display to match current selection
*/
updateMethodTrigger: function($select, $trigger) {
var $selectedOption = $select.find('option:selected');
var selectedIcon = $selectedOption.data('icon') || 'arrow_drop_down';
var selectedLabel = $selectedOption.text();
$trigger.find('.method-trigger-icon').replaceWith(this.esIcon(selectedIcon, 'method-trigger-icon'));
$trigger.find('.method-trigger-label').text(selectedLabel);
},
/**
* Show the method dropdown menu
*/
showMethodDropdownMenu: function($select, $trigger) {
var self = this;
this.closeMethodDropdownMenu();
var menuHtml = this.buildMethodDropdownMenuHtml($select);
var $menu = $(menuHtml);
var triggerOffset = $trigger.offset();
var triggerWidth = $trigger.outerWidth();
var triggerHeight = $trigger.outerHeight();
$menu.css({
position: 'absolute',
top: triggerOffset.top + triggerHeight + 2,
left: triggerOffset.left,
minWidth: triggerWidth,
zIndex: 10001
});
$('body').append($menu);
this.$methodDropdownMenu = $menu;
this.$methodDropdownSelect = $select;
this.$methodDropdownTrigger = $trigger;
$menu.on('click', '.method-dropdown-item', function(e) {
e.preventDefault();
e.stopPropagation();
var value = $(this).data('value');
$select.val(value).trigger('change');
self.closeMethodDropdownMenu();
});
$(document).on('click.methodDropdown', function(e) {
if (!$(e.target).closest('.method-dropdown-menu, .method-dropdown-trigger').length) {
self.closeMethodDropdownMenu();
}
});
$(document).on('keydown.methodDropdown', function(e) {
if (e.keyCode === 27) {
self.closeMethodDropdownMenu();
}
});
},
/**
* Build the dropdown menu HTML
*/
buildMethodDropdownMenuHtml: function($select) {
var self = this;
var html = '<div class="method-dropdown-menu">';
// Render ungrouped options first
$select.children('option').each(function() {
var $el = $(this);
var icon = $el.data('icon') || 'star';
var label = $el.text();
var value = $el.val();
var isSelected = $el.is(':selected');
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
html += this.esIcon(icon, 'method-item-icon');
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += self.esIcon('check', 'method-item-check');
}
html += '</div>';
});
// Render optgroups
$select.children('optgroup').each(function() {
var $optgroup = $(this);
var groupLabel = $optgroup.attr('label') || '';
html += '<div class="method-dropdown-optgroup">';
html += '<div class="method-optgroup-label">' + self.escapeHtml(groupLabel) + '</div>';
html += '<div class="method-optgroup-items">';
$optgroup.children('option').each(function() {
var $el = $(this);
var icon = $el.data('icon') || 'settings';
var label = $el.text();
var value = $el.val();
var isSelected = $el.is(':selected');
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
html += self.esIcon(icon, 'method-item-icon');
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += self.esIcon('check', 'method-item-check');
}
html += '</div>';
});
html += '</div>'; // close items
html += '</div>'; // close optgroup
});
html += '</div>';
return html;
},
/**
* Close the method dropdown menu
*/
closeMethodDropdownMenu: function() {
if (this.$methodDropdownMenu) {
this.$methodDropdownMenu.remove();
this.$methodDropdownMenu = null;
}
this.$methodDropdownSelect = null;
this.$methodDropdownTrigger = null;
$(document).off('click.methodDropdown keydown.methodDropdown');
},
/**
* Populate tiles for multi_select_tiles value picker
*/
populateTiles: function($picker, options, exclusive) {
var self = this;
var $container = $picker.find('.multi-select-tiles');
$container.empty();
if (exclusive) {
$container.attr('data-exclusive', 'true');
} else {
$container.removeAttr('data-exclusive');
}
$.each(options, function(key, optData) {
var label = typeof optData === 'object' ? optData.label : optData;
var icon = typeof optData === 'object' && optData.icon ? optData.icon : null;
var color = typeof optData === 'object' && optData.color ? optData.color : null;
var tileClass = 'tile-option';
if (color) {
tileClass += ' tile-color-' + color;
}
var $tile = $('<button>', {
type: 'button',
class: tileClass,
'data-value': key
});
if (icon) {
$tile.append($('<i>', { class: icon }));
}
$tile.append($('<span>', { class: 'tile-label', text: label }));
$container.append($tile);
});
},
/**
* Apply step/min constraints to numeric range inputs
*/
applyRangeInputConstraints: function($picker, step, min) {
var $inputs = $picker.find('.range-min-input, .range-max-input');
if (typeof step !== 'undefined' && step !== null) {
$inputs.attr('step', step);
} else {
$inputs.attr('step', 'any');
}
if (typeof min !== 'undefined' && min !== null) {
$inputs.attr('min', min);
} else {
$inputs.removeAttr('min');
}
},
/**
* Show error message on range input
*/
showRangeInputError: function($input, message) {
var $container = $input.closest('.multi-range-input-row');
$container.find('.range-input-error').remove();
$container.find('.range-min-input, .range-max-input').removeClass('has-error');
$input.addClass('has-error');
var $error = $('<span>', {
class: 'range-input-error',
text: message
});
$container.append($error);
setTimeout(function() {
$input.removeClass('has-error');
$error.fadeOut(200, function() {
$(this).remove();
});
}, 3000);
},
/**
* Load attribute groups for combination picker
*/
loadCombinationAttributeGroups: function($picker) {
var self = this;
var trans = this.config.trans || {};
var $container = $picker.find('.combination-groups-container');
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getAttributeGroups',
trait: 'TargetConditions'
},
success: function(response) {
$container.empty();
if (!response.success || !response.groups || response.groups.length === 0) {
$container.html('<span class="combination-empty">' +
self.escapeHtml(trans.no_attribute_groups || 'No attribute groups found') +
'</span>');
return;
}
response.groups.forEach(function(group) {
var $groupDiv = $('<div>', {
class: 'comb-attr-group',
'data-group-id': group.id
});
var $groupHeader = $('<div>', { class: 'comb-attr-group-header' });
$groupHeader.append($('<span>', {
class: 'comb-attr-group-name',
text: group.name
}));
$groupHeader.append($('<span>', {
class: 'comb-attr-group-count',
text: '0'
}));
var $toolbar = $('<div>', { class: 'comb-attr-toolbar' });
$toolbar.append($('<button>', {
type: 'button',
class: 'comb-toolbar-btn comb-select-all',
title: trans.select_all || 'Select all',
html: self.esIcon('check_box')
}));
$toolbar.append($('<button>', {
type: 'button',
class: 'comb-toolbar-btn comb-select-none',
title: trans.clear || 'Clear',
html: self.esIcon('check_box_outline_blank')
}));
$toolbar.append($('<input>', {
type: 'text',
class: 'comb-attr-search',
placeholder: trans.filter_results || 'Filter...'
}));
var $valuesContainer = $('<div>', {
class: 'comb-attr-values',
'data-loaded': 'false'
});
$valuesContainer.append($('<span>', {
class: 'comb-attr-loading',
html: self.esIcon('progress_activity', 'es-spin')
}));
$groupDiv.append($groupHeader);
$groupDiv.append($toolbar);
$groupDiv.append($valuesContainer);
$container.append($groupDiv);
self.loadCombinationAttributeValues($picker, group.id, $valuesContainer);
});
},
error: function() {
$container.html('<span class="combination-error">' +
self.escapeHtml(trans.error_loading || 'Error loading attribute groups') +
'</span>');
}
});
},
/**
* Load attribute values for a specific group
*/
loadCombinationAttributeValues: function($picker, groupId, $container) {
var self = this;
var trans = this.config.trans || {};
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getAttributeValues',
trait: 'TargetConditions',
id_attribute_group: groupId
},
success: function(response) {
$container.empty();
$container.attr('data-loaded', 'true');
if (!response.success || !response.values || response.values.length === 0) {
$container.html('<span class="comb-attr-empty">' +
self.escapeHtml(trans.no_values || 'No values') +
'</span>');
return;
}
response.values.forEach(function(value) {
var productCount = parseInt(value.product_count) || 0;
var $valueBtn = $('<button>', {
type: 'button',
class: 'comb-attr-value',
'data-value-id': value.id,
'data-group-id': groupId,
'data-name': value.name.toLowerCase()
});
$valueBtn.append($('<span>', {
class: 'comb-attr-value-name',
text: value.name
}));
if (productCount > 0) {
$valueBtn.append($('<span>', {
class: 'comb-attr-value-count',
text: productCount
}));
}
$container.append($valueBtn);
});
self.restoreCombinationSelections($picker);
},
error: function() {
$container.html('<span class="comb-attr-error">' +
self.escapeHtml(trans.error_loading || 'Error') +
'</span>');
}
});
},
/**
* Restore previously selected combination values from hidden input
*/
restoreCombinationSelections: function($picker) {
var $dataInput = $picker.find('.include-values-data, .exclude-values-data').first();
var dataStr = $dataInput.val() || '{}';
var data;
try {
data = JSON.parse(dataStr);
} catch (e) {
return;
}
var attributes = data.attributes || data;
var mode = data.mode || 'products';
$picker.find('.comb-mode-radio[value="' + mode + '"]').prop('checked', true);
$.each(attributes, function(groupId, valueIds) {
if (!Array.isArray(valueIds)) return;
valueIds.forEach(function(valueId) {
$picker.find('.comb-attr-value[data-group-id="' + groupId + '"][data-value-id="' + valueId + '"]')
.addClass('selected');
});
});
this.updateCombinationGroupCounts($picker);
},
/**
* Update hidden input with current combination selections
*/
updateCombinationData: function($picker) {
var attributes = {};
$picker.find('.comb-attr-value.selected').each(function() {
var groupId = $(this).data('groupId').toString();
var valueId = $(this).data('valueId');
if (!attributes[groupId]) {
attributes[groupId] = [];
}
attributes[groupId].push(valueId);
});
var $combPicker = $picker.find('.combination-attributes-picker');
var configMode = $combPicker.data('combinationMode') || this.config.combinationMode || 'products';
var mode;
if (configMode === 'toggle') {
mode = $picker.find('.comb-mode-radio:checked').val() || 'products';
} else {
mode = configMode;
}
var data = {
mode: mode,
attributes: attributes
};
var $dataInput = $picker.find('.include-values-data, .exclude-values-data').first();
$dataInput.val(JSON.stringify(data));
this.updateCombinationGroupCounts($picker);
},
/**
* Update the count badges on each attribute group
*/
updateCombinationGroupCounts: function($picker) {
$picker.find('.comb-attr-group').each(function() {
var $group = $(this);
var count = $group.find('.comb-attr-value.selected').length;
$group.find('.comb-attr-group-count').text(count);
if (count > 0) {
$group.addClass('has-selections');
} else {
$group.removeClass('has-selections');
}
});
},
/**
* Update the info placeholder based on method and block type
*/
updateMethodInfoPlaceholder: function($headerRow, method, blockType) {
var $placeholder = $headerRow.find('.method-info-placeholder');
if (!$placeholder.length) return;
$placeholder.empty();
var methodHelp = this.config.methodHelp || {};
var blockHelp = methodHelp[blockType] || methodHelp['products'] || {};
var helpContent = blockHelp[method] || this.getBuiltInMethodHelp(method);
if (helpContent) {
var $infoWrapper = $('<span>', {
class: 'mpr-info-wrapper',
'data-details': helpContent
});
$infoWrapper.append($(this.esIcon('info')));
$placeholder.append($infoWrapper);
// Let prestashop-admin info-tooltip.js handle this element
if (window.MPRInfoTooltip) {
window.MPRInfoTooltip.init();
}
}
},
/**
* Get built-in help content for targeting methods
*/
getBuiltInMethodHelp: function(method) {
var trans = this.config.trans || {};
var html = '';
switch (method) {
case 'all':
html = '<strong>' + this.escapeHtml(trans.help_all_title || 'All Items') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_all_desc || 'Selects all items without any filtering.') + '</p>';
break;
case 'specific':
html = '<strong>' + this.escapeHtml(trans.help_specific_title || 'Specific Items') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_specific_desc || 'Search and select individual items by name, reference, or ID.') + '</p>';
break;
case 'by_category':
html = '<strong>' + this.escapeHtml(trans.help_category_title || 'By Category') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_category_desc || 'Select items belonging to specific categories. Includes subcategories.') + '</p>';
break;
case 'by_manufacturer':
html = '<strong>' + this.escapeHtml(trans.help_manufacturer_title || 'By Manufacturer') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_manufacturer_desc || 'Select items from specific manufacturers/brands.') + '</p>';
break;
case 'by_supplier':
html = '<strong>' + this.escapeHtml(trans.help_supplier_title || 'By Supplier') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_supplier_desc || 'Select items from specific suppliers.') + '</p>';
break;
case 'by_tag':
html = '<strong>' + this.escapeHtml(trans.help_tag_title || 'By Tag') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_tag_desc || 'Select items with specific tags assigned.') + '</p>';
break;
case 'by_attribute':
html = '<strong>' + this.escapeHtml(trans.help_attribute_title || 'By Attribute') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_attribute_desc || 'Select items with specific attribute values (e.g., Color: Red).') + '</p>';
break;
case 'by_feature':
html = '<strong>' + this.escapeHtml(trans.help_feature_title || 'By Feature') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_feature_desc || 'Select items with specific feature values (e.g., Material: Cotton).') + '</p>';
break;
case 'by_combination':
html = '<strong>' + this.escapeHtml(trans.help_combination_title || 'Combination Targeting') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_combination_desc || 'Select items by combination attributes.') + '</p>';
html += '<p><strong>' + this.escapeHtml(trans.help_combination_logic || 'Logic:') + '</strong></p>';
html += '<ul>';
html += '<li>' + this.escapeHtml(trans.help_combination_within || 'Within group: OR (Red OR Blue)') + '</li>';
html += '<li>' + this.escapeHtml(trans.help_combination_between || 'Between groups: AND (Color AND Size)') + '</li>';
html += '</ul>';
break;
case 'by_carrier':
html = '<strong>' + this.escapeHtml(trans.help_carrier_title || 'By Carrier') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_carrier_desc || 'Select items available with specific carriers.') + '</p>';
break;
case 'by_condition':
html = '<strong>' + this.escapeHtml(trans.help_condition_title || 'By Condition') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_condition_desc || 'Filter by product condition: New, Used, or Refurbished.') + '</p>';
break;
case 'by_visibility':
html = '<strong>' + this.escapeHtml(trans.help_visibility_title || 'By Visibility') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_visibility_desc || 'Filter by where products are visible in the store.') + '</p>';
break;
case 'by_active_status':
html = '<strong>' + this.escapeHtml(trans.help_active_title || 'By Active Status') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_active_desc || 'Filter by whether products are enabled or disabled.') + '</p>';
break;
case 'by_stock_status':
html = '<strong>' + this.escapeHtml(trans.help_stock_title || 'By Stock Status') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_stock_desc || 'Filter by stock availability: In stock, Out of stock, or Low stock.') + '</p>';
break;
case 'by_on_sale':
case 'by_has_specific_price':
case 'by_is_virtual':
case 'by_is_pack':
case 'by_has_combinations':
case 'by_available_for_order':
case 'by_online_only':
case 'by_has_related':
case 'by_has_customization':
case 'by_has_attachments':
case 'by_has_additional_shipping':
html = '<strong>' + this.escapeHtml(trans.help_boolean_title || 'Yes/No Filter') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_boolean_desc || 'Filter products by this property.') + '</p>';
break;
case 'by_name_pattern':
case 'by_reference_pattern':
case 'by_description_pattern':
case 'by_long_description_pattern':
case 'by_ean13_pattern':
case 'by_upc_pattern':
case 'by_isbn_pattern':
case 'by_mpn_pattern':
case 'by_meta_title_pattern':
case 'by_meta_description_pattern':
html = '<strong>' + this.escapeHtml(trans.help_pattern_title || 'Pattern Matching') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_pattern_desc || 'Match text using patterns with wildcards.') + '</p>';
html += '<div><code>*</code> ' + this.escapeHtml(trans.help_pattern_wildcard || 'any text') + '</div>';
html += '<div><code>{number}</code> ' + this.escapeHtml(trans.help_pattern_number || 'any number') + '</div>';
html += '<div><code>{letter}</code> ' + this.escapeHtml(trans.help_pattern_letter || 'single letter A-Z') + '</div>';
break;
case 'by_id_range':
case 'by_price_range':
case 'by_weight_range':
case 'by_quantity_range':
case 'by_position_range':
html = '<strong>' + this.escapeHtml(trans.help_range_title || 'Numeric Range') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_range_desc || 'Filter by numeric values within specified ranges.') + '</p>';
html += '<p>' + this.escapeHtml(trans.help_range_tip || 'Leave min or max empty for open-ended ranges.') + '</p>';
break;
case 'by_date_added':
case 'by_date_updated':
html = '<strong>' + this.escapeHtml(trans.help_date_title || 'Date Range') + '</strong>';
html += '<p>' + this.escapeHtml(trans.help_date_desc || 'Filter by date within a specific period.') + '</p>';
break;
default:
break;
}
return html;
},
/**
* Build sort options HTML for a specific block type
*/
buildSortOptions: function(blockType) {
var options = [];
switch (blockType) {
case 'products':
options = [
{ value: 'sales', label: 'Best sellers' },
{ value: 'date_add', label: 'Newest' },
{ value: 'price', label: 'Price' },
{ value: 'name', label: 'Name' },
{ value: 'position', label: 'Position' },
{ value: 'quantity', label: 'Stock quantity' },
{ value: 'random', label: 'Random' }
];
break;
case 'categories':
options = [
{ value: 'name', label: 'Name' },
{ value: 'position', label: 'Position' },
{ value: 'product_count', label: 'Product count' },
{ value: 'total_sales', label: 'Best sellers' },
{ value: 'newest_products', label: 'Newest products' },
{ value: 'date_add', label: 'Creation date' },
{ value: 'random', label: 'Random' }
];
break;
case 'manufacturers':
case 'suppliers':
options = [
{ value: 'name', label: 'Name' },
{ value: 'product_count', label: 'Product count' },
{ value: 'total_sales', label: 'Best sellers' },
{ value: 'newest_products', label: 'Newest products' },
{ value: 'random', label: 'Random' }
];
break;
case 'cms':
case 'cms_categories':
options = [
{ value: 'name', label: 'Name' },
{ value: 'position', label: 'Position' },
{ value: 'random', label: 'Random' }
];
break;
default:
options = [
{ value: 'name', label: 'Name' },
{ value: 'random', label: 'Random' }
];
}
var html = '';
for (var i = 0; i < options.length; i++) {
html += '<option value="' + this.escapeAttr(options[i].value) + '">' +
this.escapeHtml(options[i].label) + '</option>';
}
return html;
},
/**
* Update the modifier toggle button state
*/
updateModifierButtonState: function($group) {
var limit = $group.find('.group-modifier-limit').val();
var sortBy = $group.find('.group-modifier-sort').val();
var $modifiers = $group.find('.group-modifiers');
var $btn = $group.find('.btn-toggle-modifiers');
var trans = this.config.trans || {};
$btn.find('.modifier-summary').remove();
if (limit || sortBy) {
$modifiers.addClass('has-values');
var summary = [];
if (limit) {
summary.push((trans.top || 'Top') + ' ' + limit);
}
if (sortBy) {
var sortLabel = $group.find('.group-modifier-sort option:selected').text();
summary.push(sortLabel);
}
var $arrow = $btn.find('.toggle-arrow');
$('<span class="modifier-summary">' + this.escapeHtml(summary.join(', ')) + '</span>').insertBefore($arrow);
} else {
$modifiers.removeClass('has-values');
}
},
/**
* Lock/unlock method selector when excludes are present
*/
updateMethodSelectorLock: function($group, locked) {
var $select = $group.find('.include-method-select');
var $wrapper = $select.closest('.method-selector-wrapper');
var trans = this.config.trans || {};
if (locked) {
$select.prop('disabled', true);
if (!$wrapper.length) {
$select.wrap('<div class="method-selector-wrapper"></div>');
$wrapper = $select.parent('.method-selector-wrapper');
}
$wrapper.addClass('selector-locked');
if (!$wrapper.find('.lock-indicator').length) {
var lockHtml = '<span class="mpr-info-wrapper lock-indicator">' +
self.esIcon('lock') +
'<span class="mpr-tooltip">' +
(trans.remove_excludes_first || 'Remove all exceptions to change selection type') +
'</span>' +
'</span>';
var $countEl = $wrapper.find('.condition-match-count');
if ($countEl.length) {
$countEl.before(lockHtml);
} else {
$wrapper.append(lockHtml);
}
}
} else {
$select.prop('disabled', false);
if ($wrapper.length) {
$wrapper.removeClass('selector-locked');
$wrapper.find('.mpr-info-wrapper.lock-indicator').remove();
} else {
$select.siblings('.mpr-info-wrapper.lock-indicator').remove();
}
}
}
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,881 @@
/**
* Entity Selector - Search Module
* AJAX search, results rendering, category tree, filters, search history
* @partial _search.js
*/
(function($) {
'use strict';
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
window._EntitySelectorMixins.search = {
// Category tree cache
categoryTreeCache: null,
/**
* Perform AJAX search for entities
*/
performSearch: function(appendMode) {
var self = this;
if (!this.activeGroup) return;
this.isLoading = true;
var searchEntity = this.activeGroup.searchEntity;
// Build request data with sort and filter params
var limit = appendMode && this.loadMoreCount ? this.loadMoreCount : 20;
var requestData = {
ajax: 1,
action: 'searchTargetEntities',
trait: 'EntitySelector',
entity_type: searchEntity,
q: this.searchQuery,
limit: limit,
offset: appendMode ? this.searchOffset : 0,
sort_by: this.currentSort ? this.currentSort.field : 'name',
sort_dir: this.currentSort ? this.currentSort.dir : 'ASC'
};
// Add refine query if present
if (this.refineQuery) {
requestData.refine = this.refineQuery;
if (this.refineNegate) {
requestData.refine_negate = 1;
}
}
// Add product-specific filters
if (searchEntity === 'products' && this.filters) {
if (this.filters.inStock) {
requestData.filter_in_stock = 1;
}
if (this.filters.discounted) {
requestData.filter_discounted = 1;
}
if (this.filters.priceMin !== null && this.filters.priceMin !== '') {
requestData.filter_price_min = this.filters.priceMin;
}
if (this.filters.priceMax !== null && this.filters.priceMax !== '') {
requestData.filter_price_max = this.filters.priceMax;
}
if (this.filters.attributes && this.filters.attributes.length > 0) {
requestData.filter_attributes = JSON.stringify(this.filters.attributes);
}
if (this.filters.features && this.filters.features.length > 0) {
requestData.filter_features = JSON.stringify(this.filters.features);
}
}
// Add entity-specific filters for non-product entities
if (searchEntity !== 'products' && this.filters) {
// Product count range (categories, manufacturers, suppliers, attributes, features)
if (this.filters.productCountMin !== null && this.filters.productCountMin !== '') {
requestData.filter_product_count_min = this.filters.productCountMin;
}
if (this.filters.productCountMax !== null && this.filters.productCountMax !== '') {
requestData.filter_product_count_max = this.filters.productCountMax;
}
// Category-specific
if (searchEntity === 'categories') {
if (this.filters.depth) {
requestData.filter_depth = this.filters.depth;
}
if (this.filters.hasProducts) {
requestData.filter_has_products = 1;
}
if (this.filters.hasDescription) {
requestData.filter_has_description = 1;
}
if (this.filters.hasImage) {
requestData.filter_has_image = 1;
}
if (this.filters.salesMin !== null && this.filters.salesMin !== '') {
requestData.filter_sales_min = this.filters.salesMin;
}
if (this.filters.salesMax !== null && this.filters.salesMax !== '') {
requestData.filter_sales_max = this.filters.salesMax;
}
if (this.filters.turnoverMin !== null && this.filters.turnoverMin !== '') {
requestData.filter_turnover_min = this.filters.turnoverMin;
}
if (this.filters.turnoverMax !== null && this.filters.turnoverMax !== '') {
requestData.filter_turnover_max = this.filters.turnoverMax;
}
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
}
// Manufacturer-specific
if (searchEntity === 'manufacturers') {
if (this.filters.salesMin !== null && this.filters.salesMin !== '') {
requestData.filter_sales_min = this.filters.salesMin;
}
if (this.filters.salesMax !== null && this.filters.salesMax !== '') {
requestData.filter_sales_max = this.filters.salesMax;
}
if (this.filters.turnoverMin !== null && this.filters.turnoverMin !== '') {
requestData.filter_turnover_min = this.filters.turnoverMin;
}
if (this.filters.turnoverMax !== null && this.filters.turnoverMax !== '') {
requestData.filter_turnover_max = this.filters.turnoverMax;
}
if (this.filters.dateAddFrom) {
requestData.filter_date_add_from = this.filters.dateAddFrom;
}
if (this.filters.dateAddTo) {
requestData.filter_date_add_to = this.filters.dateAddTo;
}
if (this.filters.lastProductFrom) {
requestData.filter_last_product_from = this.filters.lastProductFrom;
}
if (this.filters.lastProductTo) {
requestData.filter_last_product_to = this.filters.lastProductTo;
}
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
}
// Supplier-specific
if (searchEntity === 'suppliers') {
if (this.filters.salesMin !== null && this.filters.salesMin !== '') {
requestData.filter_sales_min = this.filters.salesMin;
}
if (this.filters.salesMax !== null && this.filters.salesMax !== '') {
requestData.filter_sales_max = this.filters.salesMax;
}
if (this.filters.turnoverMin !== null && this.filters.turnoverMin !== '') {
requestData.filter_turnover_min = this.filters.turnoverMin;
}
if (this.filters.turnoverMax !== null && this.filters.turnoverMax !== '') {
requestData.filter_turnover_max = this.filters.turnoverMax;
}
if (this.filters.dateAddFrom) {
requestData.filter_date_add_from = this.filters.dateAddFrom;
}
if (this.filters.dateAddTo) {
requestData.filter_date_add_to = this.filters.dateAddTo;
}
if (this.filters.lastProductFrom) {
requestData.filter_last_product_from = this.filters.lastProductFrom;
}
if (this.filters.lastProductTo) {
requestData.filter_last_product_to = this.filters.lastProductTo;
}
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
}
// Attribute-specific
if (searchEntity === 'attributes') {
if (this.filters.salesMin !== null && this.filters.salesMin !== '') {
requestData.filter_sales_min = this.filters.salesMin;
}
if (this.filters.salesMax !== null && this.filters.salesMax !== '') {
requestData.filter_sales_max = this.filters.salesMax;
}
if (this.filters.turnoverMin !== null && this.filters.turnoverMin !== '') {
requestData.filter_turnover_min = this.filters.turnoverMin;
}
if (this.filters.turnoverMax !== null && this.filters.turnoverMax !== '') {
requestData.filter_turnover_max = this.filters.turnoverMax;
}
if (this.filters.attributeGroup) {
requestData.filter_attribute_group = this.filters.attributeGroup;
}
if (this.filters.isColor) {
requestData.filter_is_color = 1;
}
}
// Feature-specific
if (searchEntity === 'features') {
if (this.filters.salesMin !== null && this.filters.salesMin !== '') {
requestData.filter_sales_min = this.filters.salesMin;
}
if (this.filters.salesMax !== null && this.filters.salesMax !== '') {
requestData.filter_sales_max = this.filters.salesMax;
}
if (this.filters.turnoverMin !== null && this.filters.turnoverMin !== '') {
requestData.filter_turnover_min = this.filters.turnoverMin;
}
if (this.filters.turnoverMax !== null && this.filters.turnoverMax !== '') {
requestData.filter_turnover_max = this.filters.turnoverMax;
}
if (this.filters.featureGroup) {
requestData.filter_feature_group = this.filters.featureGroup;
}
if (this.filters.isCustom) {
requestData.filter_is_custom = 1;
}
}
// CMS-specific
if (searchEntity === 'cms') {
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
if (this.filters.indexable) {
requestData.filter_indexable = 1;
}
}
// CMS Categories-specific
if (searchEntity === 'cms_categories') {
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
}
// Countries-specific
if (searchEntity === 'countries') {
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
if (this.filters.hasHolidays) {
requestData.filter_has_holidays = 1;
}
if (this.filters.containsStates) {
requestData.filter_contains_states = 1;
}
if (this.filters.zone) {
requestData.filter_zone = this.filters.zone;
}
}
}
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: requestData,
success: function(response) {
self.isLoading = false;
if (!response.success) return;
// Save to search history if query is not empty and has results
if (self.searchQuery && self.searchQuery.length >= 2 && response.total > 0) {
self.addToSearchHistory(searchEntity, self.searchQuery);
}
if (appendMode) {
self.searchResults = self.searchResults.concat(response.results || []);
} else {
self.searchResults = response.results || [];
}
self.searchTotal = response.total || 0;
self.searchOffset = appendMode ? self.searchOffset + (response.results || []).length : (response.results || []).length;
self.renderSearchResults(appendMode);
self.$dropdown.addClass('show');
},
error: function() {
self.isLoading = false;
}
});
},
/**
* Render search results in the dropdown
*/
renderSearchResults: function(appendMode) {
var self = this;
var trans = this.config.trans || {};
var $container = this.$dropdown.find('.dropdown-results');
// Get selected IDs from current picker (to mark as selected)
// and hidden IDs from sibling exclude pickers with same entity type (to hide completely)
var selectedIds = [];
var hiddenIds = [];
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 + '"]');
var currentSearchEntity = this.activeGroup.searchEntity;
var currentExcludeIndex = this.activeGroup.excludeIndex;
if (this.activeGroup.section === 'include') {
// For include section, just get current picker's selections
var $picker = $group.find('.include-picker');
$picker.find('.entity-chip').each(function() {
selectedIds.push(String($(this).data('id')));
});
} else {
// For exclude section, get current picker's selections AND
// collect IDs from sibling exclude rows with same entity type to hide
var $currentExcludeRow = $group.find('.exclude-row[data-exclude-index="' + currentExcludeIndex + '"]');
var $currentPicker = $currentExcludeRow.find('.exclude-picker');
// Get selected IDs from current exclude row
$currentPicker.find('.entity-chip').each(function() {
selectedIds.push(String($(this).data('id')));
});
// Get hidden IDs from OTHER exclude rows with the same entity type
$group.find('.exclude-row').each(function() {
var $row = $(this);
var rowIndex = parseInt($row.data('excludeIndex'), 10);
// Skip current exclude row
if (rowIndex === currentExcludeIndex) return;
var $picker = $row.find('.exclude-picker');
var rowEntityType = $picker.attr('data-search-entity') || self.activeGroup.blockType;
// Only collect if same entity type
if (rowEntityType === currentSearchEntity) {
$picker.find('.entity-chip').each(function() {
hiddenIds.push(String($(this).data('id')));
});
}
});
}
}
// Check if this is a product search
var isProductSearch = this.activeGroup && this.activeGroup.searchEntity === 'products';
var isListView = this.viewMode === 'list';
// Show/hide results header for products in list view
this.$dropdown.find('.results-header').toggle(isProductSearch && isListView);
// Build HTML - filter out items that are hidden (selected in sibling exclude rows)
var visibleResults = this.searchResults.filter(function(item) {
return hiddenIds.indexOf(String(item.id)) === -1;
});
// Update count (show visible count and total, noting hidden items if any)
var hiddenCount = this.searchResults.length - visibleResults.length;
var countText = visibleResults.length + ' / ' + this.searchTotal + ' results';
if (hiddenCount > 0) {
countText += ' (' + hiddenCount + ' hidden)';
}
this.$dropdown.find('.results-count').text(countText);
var html = '';
if (visibleResults.length === 0 && !appendMode) {
html = '<div class="no-results">' + this.esIcon('search') + ' ' + (trans.no_results || 'No results found') + '</div>';
} else {
visibleResults.forEach(function(item) {
var isSelected = selectedIds.indexOf(String(item.id)) !== -1;
var itemClass = 'dropdown-item' + (isSelected ? ' selected' : '');
if (item.type === 'product') itemClass += ' result-item-product';
html += '<div class="' + itemClass + '" ';
html += 'data-id="' + self.escapeAttr(item.id) + '" ';
html += 'data-name="' + self.escapeAttr(item.name) + '"';
if (item.image) html += ' data-image="' + self.escapeAttr(item.image) + '"';
if (item.subtitle) html += ' data-subtitle="' + self.escapeAttr(item.subtitle) + '"';
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
html += '>';
html += '<span class="result-checkbox">' + self.esIcon('check') + '</span>';
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : null;
// Countries show flags
if (searchEntity === 'countries' && item.iso_code) {
var flagUrl = 'https://flagcdn.com/w40/' + item.iso_code.toLowerCase() + '.png';
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;">' + self.esIcon('flag') + '</span></div>';
} else if (item.image) {
html += '<div class="result-image"><img src="' + self.escapeAttr(item.image) + '" alt=""></div>';
} else {
// Entity-specific icons
var iconName = 'widgets'; // default
if (searchEntity === 'categories') iconName = 'folder';
else if (searchEntity === 'manufacturers') iconName = 'business';
else if (searchEntity === 'suppliers') iconName = 'local_shipping';
else if (searchEntity === 'attributes') iconName = 'brush';
else if (searchEntity === 'features') iconName = 'list';
else if (searchEntity === 'cms') iconName = 'description';
else if (searchEntity === 'cms_categories') iconName = 'folder';
html += '<div class="result-icon">' + self.esIcon(iconName) + '</div>';
}
html += '<div class="result-info">';
html += '<div class="result-name">' + self.escapeHtml(item.name) + '</div>';
if (item.subtitle) {
// Split multi-line subtitles into separate divs for styling
var subtitleLines = item.subtitle.split('\n');
html += '<div class="result-subtitle">';
subtitleLines.forEach(function(line, idx) {
var lineClass = idx === 0 ? 'subtitle-line subtitle-line-primary' : 'subtitle-line subtitle-line-secondary';
html += '<div class="' + lineClass + '">' + self.escapeHtml(line) + '</div>';
});
html += '</div>';
}
html += '</div>';
// Add product-specific columns (price, sale price, stock, sold)
if (item.type === 'product') {
if (isListView) {
// List view: full columns
// Regular price
html += '<div class="result-col result-col-price">';
html += '<span class="col-value">' + (item.regular_price_formatted || item.price_formatted || '') + '</span>';
html += '</div>';
// Sale price (only if discounted)
if (item.has_discount) {
html += '<div class="result-col result-col-sale">';
html += '<span class="col-value">' + (item.price_formatted || '') + '</span>';
html += '</div>';
} else {
html += '<div class="result-col result-col-sale"></div>';
}
// Stock column
var stockClass = item.stock_status === 'out_of_stock' ? 'stock-out' :
(item.stock_status === 'low_stock' ? 'stock-low' : 'stock-ok');
html += '<div class="result-col result-col-stock">';
html += '<span class="col-value ' + stockClass + '">' + (item.stock_qty !== undefined ? item.stock_qty : '') + '</span>';
html += '</div>';
// Sales column
html += '<div class="result-col result-col-sales">';
html += '<span class="col-value">' + (item.sales_qty !== undefined ? item.sales_qty : '0') + '</span>';
html += '</div>';
} else {
// Grid view: compact info line
var gridStockClass = item.stock_status === 'out_of_stock' ? 'stock-out' :
(item.stock_status === 'low_stock' ? 'stock-low' : '');
html += '<div class="result-grid-info">';
html += '<span class="grid-price">' + (item.price_formatted || '') + '</span>';
if (item.stock_qty !== undefined) {
html += '<span class="grid-stock ' + gridStockClass + '">' + item.stock_qty + ' qty</span>';
}
if (item.has_discount) {
html += '<span class="grid-discount">-' + (item.discount_percent || '') + '%</span>';
}
html += '</div>';
}
}
html += '</div>';
});
}
if (appendMode) {
$container.append(html);
} else {
$container.html(html);
}
// Show/hide load more controls and update remaining count
var hasMore = this.searchResults.length < this.searchTotal;
var $loadMoreControls = this.$dropdown.find('.load-more-controls');
$loadMoreControls.toggle(hasMore);
if (hasMore) {
var remaining = this.searchTotal - this.searchResults.length;
$loadMoreControls.find('.remaining-count').text(remaining);
// Update "All" option in dropdown
var $select = $loadMoreControls.find('.load-more-select');
var $allOption = $select.find('option[data-all="true"]');
if ($allOption.length) {
$allOption.val(remaining).text((trans.all || 'All') + ' (' + remaining + ')');
} else {
$select.find('option:last').after('<option value="' + remaining + '" data-all="true">' + (trans.all || 'All') + ' (' + remaining + ')</option>');
}
}
// Ensure dropdown-actions are visible and history button is deactivated
this.$dropdown.find('.dropdown-actions').show();
this.$dropdown.find('.btn-show-history').removeClass('active');
// Disable history button if no search history for current entity type
var entityType = this.activeGroup ? this.activeGroup.searchEntity : null;
var hasHistory = entityType && this.getSearchHistory(entityType).length > 0;
this.$dropdown.find('.btn-show-history').prop('disabled', !hasHistory);
},
// NOTE: Tree methods (loadCategoryTree, renderCategoryTree, filterCategoryTree,
// findTreeDescendants, findTreeAncestors, updateSelectChildrenButtons) are
// defined in _tree.js which is merged later and takes precedence.
// =========================================================================
// Search History
// =========================================================================
loadSearchHistory: function() {
try {
var stored = localStorage.getItem(this.searchHistoryKey);
this.searchHistory = stored ? JSON.parse(stored) : {};
} catch (e) {
this.searchHistory = {};
}
},
saveSearchHistory: function() {
try {
localStorage.setItem(this.searchHistoryKey, JSON.stringify(this.searchHistory));
} catch (e) {
// localStorage might be full or unavailable
}
},
addToSearchHistory: function(entityType, query) {
if (!query || query.length < 2) return;
if (!this.searchHistory[entityType]) {
this.searchHistory[entityType] = [];
}
var history = this.searchHistory[entityType];
// Remove if already exists (will re-add at top)
var existingIndex = history.indexOf(query);
if (existingIndex !== -1) {
history.splice(existingIndex, 1);
}
// Add at beginning
history.unshift(query);
// Trim to max
if (history.length > this.searchHistoryMax) {
history = history.slice(0, this.searchHistoryMax);
}
this.searchHistory[entityType] = history;
this.saveSearchHistory();
},
removeFromSearchHistory: function(entityType, query) {
if (!this.searchHistory[entityType]) return;
var index = this.searchHistory[entityType].indexOf(query);
if (index !== -1) {
this.searchHistory[entityType].splice(index, 1);
this.saveSearchHistory();
}
},
getSearchHistory: function(entityType) {
return this.searchHistory[entityType] || [];
},
showSearchHistory: function(entityType) {
var history = this.getSearchHistory(entityType);
var trans = this.config.trans || {};
var $container = this.$dropdown.find('.dropdown-results');
// Update header
this.$dropdown.find('.results-count').text(trans.recent_searches || 'Recent searches');
// Hide filters, actions, and results header for history view
this.$dropdown.find('.dropdown-actions').hide();
this.$dropdown.find('.filter-panel').removeClass('show');
this.$dropdown.find('.btn-toggle-filters').removeClass('active');
this.$dropdown.find('.results-header').hide();
if (!history.length) {
// No history - just do a regular search
this.performSearch();
return;
}
// Build history items
var html = '<div class="search-history-list">';
for (var i = 0; i < history.length; i++) {
var query = history[i];
html += '<div class="history-item" data-query="' + this.escapeAttr(query) + '">';
html += this.esIcon('schedule');
html += '<span class="history-query">' + this.escapeHtml(query) + '</span>';
html += '<button type="button" class="btn-delete-history" title="' + (trans.remove || 'Remove') + '">';
html += this.esIcon('close');
html += '</button>';
html += '</div>';
}
html += '</div>';
$container.html(html);
this.$dropdown.addClass('show');
},
// =========================================================================
// Filter Methods
// =========================================================================
refreshSearch: function() {
// In tree view mode, re-filter the tree instead of doing a flat AJAX search
if (this.viewMode === 'tree') {
this.filterCategoryTree(this.searchQuery || '');
return;
}
this.searchOffset = 0;
this.loadMoreCount = 20;
// Reset load more select to default
if (this.$dropdown) {
this.$dropdown.find('.load-more-select').val('20');
// Remove the dynamic "All" option
this.$dropdown.find('.load-more-select option[data-all="true"]').remove();
}
this.performSearch(false);
},
clearFilters: function() {
this.refineQuery = '';
this.refineNegate = false;
this.filters = {
inStock: false,
discounted: false,
priceMin: null,
priceMax: null,
attributes: [],
features: [],
// Entity-specific filters
productCountMin: null,
productCountMax: null,
salesMin: null,
salesMax: null,
turnoverMin: null,
turnoverMax: null,
depth: null,
hasProducts: false,
hasDescription: false,
hasImage: false,
activeOnly: true,
attributeGroup: null,
featureGroup: null,
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
};
if (this.$dropdown) {
var trans = this.config.trans || {};
this.$dropdown.find('.refine-input').val('').attr('placeholder', trans.refine_short || 'Refine...');
this.$dropdown.find('.btn-clear-refine').hide();
this.$dropdown.find('.btn-refine-negate').removeClass('active');
this.$dropdown.find('.filter-in-stock').prop('checked', false);
this.$dropdown.find('.filter-discounted').prop('checked', false);
this.$dropdown.find('.filter-price-min').val('');
this.$dropdown.find('.filter-price-max').val('');
this.$dropdown.find('.filter-attr-chip').removeClass('active');
this.$dropdown.find('.filter-feat-chip').removeClass('active');
this.$dropdown.find('.filter-group-toggle').removeClass('active has-selection');
this.$dropdown.find('.filter-row-values').hide();
// Clear entity-specific filter inputs
this.$dropdown.find('.filter-product-count-min, .filter-product-count-max').val('');
this.$dropdown.find('.filter-sales-min, .filter-sales-max').val('');
this.$dropdown.find('.filter-turnover-min, .filter-turnover-max').val('');
this.$dropdown.find('.filter-date-add-from, .filter-date-add-to').val('');
this.$dropdown.find('.filter-last-product-from, .filter-last-product-to').val('');
this.$dropdown.find('.filter-depth-select').val('');
this.$dropdown.find('.filter-has-products').prop('checked', false);
this.$dropdown.find('.filter-has-description').prop('checked', false);
this.$dropdown.find('.filter-has-image').prop('checked', false);
this.$dropdown.find('.filter-active-only').prop('checked', true);
this.$dropdown.find('.filter-attribute-group-select, .filter-feature-group-select').val('');
// Country filters
this.$dropdown.find('.filter-has-holidays').prop('checked', false);
this.$dropdown.find('.filter-contains-states').prop('checked', false);
this.$dropdown.find('.filter-zone-select').val('');
}
this.refreshSearch();
},
// Reset filters without triggering a search (used when switching entity types)
resetFiltersWithoutSearch: function() {
this.refineQuery = '';
this.refineNegate = false;
this.filters = {
inStock: false,
discounted: false,
priceMin: null,
priceMax: null,
attributes: [],
features: [],
productCountMin: null,
productCountMax: null,
salesMin: null,
salesMax: null,
turnoverMin: null,
turnoverMax: null,
depth: null,
hasProducts: false,
hasDescription: false,
hasImage: false,
activeOnly: true,
attributeGroup: null,
featureGroup: null,
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
};
if (this.$dropdown) {
var trans = this.config.trans || {};
this.$dropdown.find('.refine-input').val('').attr('placeholder', trans.refine_short || 'Refine...');
this.$dropdown.find('.btn-clear-refine').hide();
this.$dropdown.find('.btn-refine-negate').removeClass('active');
this.$dropdown.find('.filter-in-stock').prop('checked', false);
this.$dropdown.find('.filter-discounted').prop('checked', false);
this.$dropdown.find('.filter-price-min').val('');
this.$dropdown.find('.filter-price-max').val('');
this.$dropdown.find('.filter-attr-chip').removeClass('active');
this.$dropdown.find('.filter-feat-chip').removeClass('active');
this.$dropdown.find('.filter-group-toggle').removeClass('active has-selection');
this.$dropdown.find('.filter-row-values').hide();
this.$dropdown.find('.filter-product-count-min, .filter-product-count-max').val('');
this.$dropdown.find('.filter-sales-min, .filter-sales-max').val('');
this.$dropdown.find('.filter-turnover-min, .filter-turnover-max').val('');
this.$dropdown.find('.filter-date-add-from, .filter-date-add-to').val('');
this.$dropdown.find('.filter-last-product-from, .filter-last-product-to').val('');
this.$dropdown.find('.filter-depth-select').val('');
this.$dropdown.find('.filter-has-products').prop('checked', false);
this.$dropdown.find('.filter-has-description').prop('checked', false);
this.$dropdown.find('.filter-has-image').prop('checked', false);
this.$dropdown.find('.filter-active-only').prop('checked', true);
this.$dropdown.find('.filter-attribute-group-select, .filter-feature-group-select').val('');
// Country filters
this.$dropdown.find('.filter-has-holidays').prop('checked', false);
this.$dropdown.find('.filter-contains-states').prop('checked', false);
this.$dropdown.find('.filter-zone-select').val('');
}
// Note: Does NOT call refreshSearch() - caller handles search/load
},
updateFilterPanelForEntity: function(entityType) {
if (!this.$dropdown) {
return;
}
var $panel = this.$dropdown.find('.filter-panel');
// Hide all filter rows first
$panel.find('.filter-row').hide();
// Show/hide tree view option based on entity type
var $treeOption = this.$dropdown.find('.view-mode-select option.tree-view-option');
if (entityType === 'categories' || entityType === 'cms_categories') {
$treeOption.prop('disabled', false).prop('hidden', false);
// Auto-switch to tree view for categories
if (this.viewMode !== 'tree') {
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');
this.loadCategoryTree();
} else {
this.loadCategoryTree();
}
} else {
$treeOption.prop('disabled', true).prop('hidden', true);
// If currently in tree mode, switch back to list
if (this.viewMode === 'tree') {
this.viewMode = 'list';
this.$dropdown.find('.view-mode-select').val('list');
this.$dropdown.removeClass('view-tree').addClass('view-list');
}
}
// Show entity-specific filter row (prepare visibility, but don't auto-expand panel)
if (entityType === 'products') {
// Prepare the correct rows to be visible when panel is shown
$panel.find('.filter-row-quick').show();
// Show attribute/feature rows if we have cached data
if (this.filterableData) {
if (this.filterableData.attributes && this.filterableData.attributes.length > 0) {
this.$dropdown.find('.filter-row-attributes').show();
}
if (this.filterableData.features && this.filterableData.features.length > 0) {
this.$dropdown.find('.filter-row-features').show();
}
}
} else if (entityType === 'categories') {
$panel.find('.filter-row-entity-categories').show();
} else if (entityType === 'manufacturers') {
$panel.find('.filter-row-entity-manufacturers').show();
} else if (entityType === 'suppliers') {
$panel.find('.filter-row-entity-suppliers').show();
} else if (entityType === 'attributes') {
$panel.find('.filter-row-entity-attributes').show();
this.loadAttributeGroups();
} else if (entityType === 'features') {
$panel.find('.filter-row-entity-features').show();
} else if (entityType === 'cms') {
$panel.find('.filter-row-entity-cms').show();
} else if (entityType === 'cms_categories') {
$panel.find('.filter-row-entity-cms-categories').show();
} else if (entityType === 'countries') {
$panel.find('.filter-row-entity-countries').show();
this.loadZonesForCountryFilter();
}
},
loadAttributeGroups: function() {
var self = this;
var $select = this.$dropdown.find('.filter-attribute-group-select');
// Already loaded?
if ($select.find('option').length > 1) return;
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getAttributeGroups',
trait: 'EntitySelector'
},
success: function(response) {
if (response.success && response.groups) {
$.each(response.groups, function(i, group) {
$select.append('<option value="' + group.id + '">' + self.escapeHtml(group.name) + ' (' + group.count + ')</option>');
});
}
}
});
},
loadFeatureGroups: function() {
var self = this;
var $select = this.$dropdown.find('.filter-feature-group-select');
// Already loaded?
if ($select.find('option').length > 1) return;
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getFeatureGroups',
trait: 'EntitySelector'
},
success: function(response) {
if (response.success && response.groups) {
$.each(response.groups, function(i, group) {
$select.append('<option value="' + group.id + '">' + self.escapeHtml(group.name) + ' (' + group.count + ')</option>');
});
}
}
});
}
};
})(jQuery);

View 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">' + this.esIcon('progress_activity', 'es-spin') + ' ' +
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 += this.esIcon('add_box') + ' ' + this.escapeHtml(trans.expand_all || 'Expand all');
html += '</button>';
html += '<button type="button" class="btn-collapse-all" title="' +
this.escapeAttr(trans.collapse_all || 'Collapse all') + '">';
html += this.esIcon('indeterminate_check_box') + ' ' + 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">' + self.esIcon('arrow_drop_down') + '</span>';
// Select with children button (next to toggle on the left)
html += '<button type="button" class="btn-select-children" title="' +
self.escapeAttr(trans.select_with_children || 'Select with all children') + '">';
html += self.esIcon('check_box');
html += '</button>';
} else {
html += '<span class="tree-toggle tree-leaf"></span>';
}
// Checkbox indicator
html += '<span class="tree-checkbox">' + this.esIcon('check') + '</span>';
// Category icon
html += '<span class="tree-icon">' + this.esIcon('folder') + '</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 += self.esIcon('visibility') + ' ' + 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').replaceWith(self.esIcon('indeterminate_check_box'));
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
} else {
$btn.find('i').replaceWith(self.esIcon('add_box'));
$btn.attr('title', trans.select_with_children || 'Select with all children');
}
});
}
};
})(jQuery);

View File

@@ -0,0 +1,407 @@
/**
* Entity Selector - Utilities Module
* Helper functions: escape, validation, icons, search history
* @partial _utils.js (must be loaded first)
*
* EXTRACTION SOURCE: assets/js/admin/entity-selector.js
* Lines: 7552-7570 (escapeHtml, escapeAttr)
* 7577-7590 (getEntityTypeLabel)
* 6289-6350 (validate, showValidationError, clearValidationError)
* 7115-7137 (showRangeInputError)
* 7728-7745 (getBlockMode, isBlockSingleMode)
* 7707-7723 (getCurrentSingleSelection)
* 5411-5467 (search history methods)
*/
(function($) {
'use strict';
// Create mixin namespace
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
// ---------------------------------------------------------------
// Icon framework detection & FA4 mapping (module-level singleton)
// ---------------------------------------------------------------
var _iconMode = null;
/**
* Material Icons → FontAwesome 4 class mapping.
* FA4 uses class-based icons (icon-name), Material uses text content.
*/
var FA4_MAP = {
'account_tree': 'icon-sitemap',
'add': 'icon-plus',
'add_box': 'icon-plus-square',
'arrow_downward': 'icon-sort-desc',
'arrow_drop_down': 'icon-caret-down',
'arrow_right': 'icon-chevron-right',
'arrow_upward': 'icon-sort-asc',
'block': 'icon-ban',
'brush': 'icon-paint-brush',
'business': 'icon-building',
'check': 'icon-check',
'check_box': 'icon-check-square',
'check_box_outline_blank': 'icon-square-o',
'check_circle': 'icon-check-circle',
'close': 'icon-times',
'delete': 'icon-trash',
'description': 'icon-file-text',
'error': 'icon-exclamation-circle',
'event': 'icon-calendar',
'event_busy': 'icon-calendar-times-o',
'expand_less': 'icon-chevron-up',
'expand_more': 'icon-chevron-down',
'filter_list': 'icon-filter',
'flag': 'icon-flag',
'folder': 'icon-folder',
'folder_open': 'icon-folder-open',
'indeterminate_check_box': 'icon-minus-square',
'info': 'icon-info-circle',
'inventory_2': 'icon-archive',
'label': 'icon-tag',
'language': 'icon-globe',
'lightbulb': 'icon-lightbulb-o',
'list': 'icon-list',
'list_alt': 'icon-list-alt',
'local_shipping': 'icon-truck',
'lock': 'icon-lock',
'my_location': 'icon-crosshairs',
'open_in_full': 'icon-expand',
'payments': 'icon-credit-card',
'progress_activity': 'icon-circle-o-notch',
'schedule': 'icon-clock-o',
'search': 'icon-search',
'shopping_cart': 'icon-shopping-cart',
'shuffle': 'icon-random',
'sort': 'icon-sort',
'sort_by_alpha': 'icon-sort-alpha-asc',
'star': 'icon-star',
'sync': 'icon-refresh',
'tune': 'icon-sliders',
'visibility': 'icon-eye',
'warning': 'icon-warning',
'widgets': 'icon-th-large'
};
/**
* Reverse map: FontAwesome 4 class → Material Icons name.
* Built once lazily from FA4_MAP + extra mappings for FA4 names
* that don't appear as values in FA4_MAP.
*/
var _REVERSE_FA4_MAP = null;
var EXTRA_REVERSE_MAPPINGS = {
'icon-cube': 'inventory',
'icon-folder-o': 'folder',
'icon-file-text-o': 'description',
'icon-briefcase': 'work',
'icon-user': 'person',
'icon-users': 'group',
'icon-money': 'payments',
'icon-tasks': 'checklist',
'icon-calculator': 'calculate',
'icon-asterisk': 'star',
'icon-bar-chart': 'bar_chart',
'icon-cogs': 'settings',
'icon-cog': 'settings',
'icon-tags': 'label',
'icon-list-ul': 'list',
'icon-th': 'grid_view',
'icon-certificate': 'verified',
'icon-power-off': 'power_settings_new',
'icon-circle-o': 'radio_button_unchecked'
};
function getReverseFa4Map() {
if (_REVERSE_FA4_MAP !== null) return _REVERSE_FA4_MAP;
_REVERSE_FA4_MAP = {};
// Invert FA4_MAP: value → key
for (var material in FA4_MAP) {
if (FA4_MAP.hasOwnProperty(material)) {
var fa4Class = FA4_MAP[material];
if (!_REVERSE_FA4_MAP[fa4Class]) {
_REVERSE_FA4_MAP[fa4Class] = material;
}
}
}
// Merge extras
for (var fa4 in EXTRA_REVERSE_MAPPINGS) {
if (EXTRA_REVERSE_MAPPINGS.hasOwnProperty(fa4) && !_REVERSE_FA4_MAP[fa4]) {
_REVERSE_FA4_MAP[fa4] = EXTRA_REVERSE_MAPPINGS[fa4];
}
}
return _REVERSE_FA4_MAP;
}
/**
* Normalize an icon name — handles both Material and FA4 class names.
* @param {string} name - Icon name (Material or FA4 format)
* @param {string} mode - 'material' or 'fa4'
* @returns {object} { name: string, extra: string, rawFa4: boolean }
*/
function normalizeIconName(name, mode) {
var extra = '';
var rawFa4 = false;
if (name.indexOf('icon-') === 0) {
// FA4 class name input — may have extra classes (e.g. "icon-power-off text-success")
var spaceIdx = name.indexOf(' ');
var fa4Class = (spaceIdx !== -1) ? name.substring(0, spaceIdx) : name;
if (spaceIdx !== -1) extra = name.substring(spaceIdx + 1);
if (mode === 'material') {
var reverseMap = getReverseFa4Map();
var materialName = reverseMap[fa4Class];
if (materialName) {
return { name: materialName, extra: extra, rawFa4: false };
}
// Fallback: strip 'icon-' and convert hyphens to underscores
var fallback = fa4Class.substring(5).replace(/-/g, '_');
return { name: fallback, extra: extra, rawFa4: false };
}
// FA4 mode + FA4 input — use as-is
return { name: fa4Class, extra: extra, rawFa4: true };
}
// Material Icons name — pass through
return { name: name, extra: extra, rawFa4: false };
}
/**
* Detect icon framework: 'material' (PS 8+/9+) or 'fa4' (PS 1.6/1.7).
* Checks PHP-set data attribute first, falls back to font detection.
*/
function detectIconMode() {
if (_iconMode !== null) return _iconMode;
// 1. PHP sets data-icon-mode on the wrapper
var $w = $('.entity-selector-trait[data-icon-mode], .target-conditions-trait[data-icon-mode]').first();
if ($w.length && $w.data('icon-mode')) {
_iconMode = $w.data('icon-mode');
return _iconMode;
}
// 2. Fallback: probe whether Material Icons font is loaded
var test = document.createElement('i');
test.className = 'material-icons';
test.style.cssText = 'position:absolute;left:-9999px;top:-9999px;font-size:16px;pointer-events:none';
test.textContent = 'check';
(document.body || document.documentElement).appendChild(test);
var family = (window.getComputedStyle(test).fontFamily || '').toLowerCase();
test.parentNode.removeChild(test);
_iconMode = (family.indexOf('material') !== -1) ? 'material' : 'fa4';
return _iconMode;
}
// Utility functions mixin
window._EntitySelectorMixins.utils = {
/**
* Debounce function - delays execution until after wait milliseconds
* @param {Function} func - Function to debounce
* @param {number} wait - Milliseconds to wait
* @returns {Function} Debounced function
*/
debounce: function(func, wait) {
var timeout;
return function() {
var context = this;
var args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
},
escapeHtml: function(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
escapeAttr: function(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
/**
* Icon helper — returns HTML for an icon that works on PS 1.6 through 9.x.
* Accepts both Material Icons names and FA4 class names as input.
* Automatically uses Material Icons (PS 8+/9+) or FontAwesome 4 (PS 1.6/1.7).
*
* @param {string} name - Icon name (Material or FA4 format, e.g. 'search', 'icon-cube')
* @param {string} [extraClass] - Additional CSS class(es) (e.g. 'es-spin', 'method-trigger-icon')
* @returns {string} HTML string for an <i> element
*/
esIcon: function(name, extraClass) {
var mode = detectIconMode();
var normalized = normalizeIconName(name, mode);
var iconName = normalized.name;
// Merge extra classes from normalization
if (normalized.extra) {
extraClass = extraClass ? extraClass + ' ' + normalized.extra : normalized.extra;
}
if (mode === 'material') {
var cls = 'material-icons es-icon';
if (extraClass) cls += ' ' + extraClass;
return '<i class="' + cls + '">' + iconName + '</i>';
}
// FA4 mode
if (normalized.rawFa4) {
// Input was already an FA4 class — use directly
var cls = iconName + ' es-icon';
} else {
// Input was Material — map to FA4
var cls = (FA4_MAP[iconName] || 'icon-circle') + ' es-icon';
}
if (extraClass) cls += ' ' + extraClass;
return '<i class="' + cls + '"></i>';
},
/**
* Update an existing <i> icon element to show a different icon.
* Handles both Material Icons and FA4 modes.
* Accepts both Material Icons names and FA4 class names as input.
*
* @param {jQuery} $el - The <i> element to update
* @param {string} name - Icon name (Material or FA4 format)
* @param {string} [extraClass] - Additional CSS class(es) to preserve
*/
esIconUpdate: function($el, name, extraClass) {
var mode = detectIconMode();
var normalized = normalizeIconName(name, mode);
var iconName = normalized.name;
if (normalized.extra) {
extraClass = extraClass ? extraClass + ' ' + normalized.extra : normalized.extra;
}
if (mode === 'material') {
var cls = 'material-icons es-icon';
if (extraClass) cls += ' ' + extraClass;
$el.attr('class', cls).text(iconName);
} else {
if (normalized.rawFa4) {
var cls = iconName + ' es-icon';
} else {
var cls = (FA4_MAP[iconName] || 'icon-circle') + ' es-icon';
}
if (extraClass) cls += ' ' + extraClass;
$el.attr('class', cls).text('');
}
},
getEntityTypeIcon: function(entityType) {
var icons = {
'products': 'shopping_cart',
'categories': 'folder_open',
'manufacturers': 'business',
'suppliers': 'local_shipping',
'attributes': 'list_alt',
'features': 'label',
'cms': 'description',
'cms_categories': 'folder'
};
return icons[entityType] || 'widgets';
},
getEntityTypeLabel: function(entityType) {
var trans = this.config.trans || {};
var labels = {
'products': trans.product || 'Product',
'categories': trans.category || 'Category',
'manufacturers': trans.manufacturer || 'Manufacturer',
'suppliers': trans.supplier || 'Supplier',
'attributes': trans.attribute || 'Attribute',
'features': trans.feature || 'Feature',
'cms': trans.cms_page || 'CMS Page',
'cms_categories': trans.cms_category || 'CMS Category'
};
return labels[entityType] || entityType;
},
validate: function() {
var isRequired = this.$wrapper.data('required') === 1 || this.$wrapper.data('required') === '1';
if (!isRequired) return true;
var hasData = false;
this.$wrapper.find('.target-block').each(function() {
if ($(this).find('.selection-group').length > 0) {
hasData = true;
return false;
}
});
if (!hasData) {
this.showValidationError();
return false;
}
this.clearValidationError();
return true;
},
showValidationError: function() {
this.$wrapper.addClass('has-validation-error');
var message = this.$wrapper.data('required-message') || 'Please select at least one item';
this.$wrapper.find('.trait-validation-error').remove();
var $error = $('<div>', {
class: 'trait-validation-error',
html: this.esIcon('warning') + ' ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
$('html, body').animate({ scrollTop: this.$wrapper.offset().top - 100 }, 300);
if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
this.$wrapper.find('.condition-trait-body').slideDown(200);
this.$wrapper.removeClass('collapsed');
}
},
clearValidationError: function() {
this.$wrapper.removeClass('has-validation-error');
this.$wrapper.find('.trait-validation-error').remove();
},
getBlockMode: function(blockType) {
var blockDef = this.config.blocks[blockType];
return (blockDef && blockDef.mode) ? blockDef.mode : 'multi';
},
isBlockSingleMode: function(blockType) {
return this.getBlockMode(blockType) === 'single';
},
getCurrentSingleSelection: function() {
if ((this.config.mode || 'multi') !== 'single') return null;
var $chip = this.$wrapper.find('.entity-chips .entity-chip').first();
if ($chip.length) {
var $block = $chip.closest('.target-block');
return {
name: $chip.find('.chip-name').text() || $chip.data('id'),
entityType: $block.data('block-type') || 'item'
};
}
return null;
},
/**
* Check if entity type supports tree browsing
*/
supportsTreeBrowsing: function(entityType) {
return entityType === 'categories' || entityType === 'cms_categories';
}
};
})(jQuery);

View File

@@ -0,0 +1,365 @@
/**
* Entity Selector - Validation Module
* Conflict detection and prevention for entity selections
* @partial _validation.js
*
* Features:
* - Same entity in include & exclude detection
* - Parent-child conflict detection for tree entities
* - Redundant selection detection
* - Error message display
*/
(function($) {
'use strict';
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
window._EntitySelectorMixins.validation = {
/**
* Validate a selection before adding it
* Returns { valid: true } or { valid: false, error: 'message', type: 'conflict_type' }
*
* @param {number|string} id - Entity ID to validate
* @param {string} name - Entity name (for error messages)
* @param {string} section - 'include' or 'exclude'
* @param {Object} data - Additional data (parent_id, etc.)
* @returns {Object} Validation result
*/
validateSelection: function(id, name, section, data) {
if (!this.activeGroup) {
return { valid: true };
}
var trans = this.config.trans || {};
id = parseInt(id, 10);
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 + '"]');
// Get include chips
var includeIds = this.getChipIds($group.find('.include-picker'));
// Get all exclude chips (from all exclude rows)
var excludeIds = [];
$group.find('.exclude-row').each(function() {
var $excludePicker = $(this).find('.exclude-picker');
var ids = [];
$excludePicker.find('.entity-chip').each(function() {
ids.push(parseInt($(this).data('id'), 10));
});
excludeIds = excludeIds.concat(ids);
});
// 1. Check for same entity in include & exclude
var conflictResult = this.checkIncludeExcludeConflict(id, name, section, includeIds, excludeIds, trans);
if (!conflictResult.valid) {
return conflictResult;
}
// 2. Check for redundant selection (already selected in same section)
var redundantResult = this.checkRedundantSelection(id, name, section, includeIds, excludeIds, trans);
if (!redundantResult.valid) {
return redundantResult;
}
// 3. Check for parent-child conflicts (only for tree entities)
var searchEntity = this.activeGroup.searchEntity;
if (searchEntity === 'categories' || searchEntity === 'cms_categories') {
var treeResult = this.checkTreeConflicts(id, name, section, data, includeIds, excludeIds, trans);
if (!treeResult.valid) {
return treeResult;
}
}
return { valid: true };
},
/**
* Check if entity is in both include and exclude
*/
checkIncludeExcludeConflict: function(id, name, section, includeIds, excludeIds, trans) {
if (section === 'include' && excludeIds.indexOf(id) !== -1) {
return {
valid: false,
type: 'include_exclude_conflict',
error: (trans.error_in_exclude || '"{name}" is already in the exclude list. Remove it from exclude first.').replace('{name}', name)
};
}
if (section === 'exclude' && includeIds.indexOf(id) !== -1) {
return {
valid: false,
type: 'include_exclude_conflict',
error: (trans.error_in_include || '"{name}" is already in the include list. Remove it from include first.').replace('{name}', name)
};
}
return { valid: true };
},
/**
* Check for redundant selection (already selected)
*/
checkRedundantSelection: function(id, name, section, includeIds, excludeIds, trans) {
if (section === 'include' && includeIds.indexOf(id) !== -1) {
return {
valid: false,
type: 'redundant',
error: (trans.error_already_selected || '"{name}" is already selected.').replace('{name}', name)
};
}
if (section === 'exclude' && excludeIds.indexOf(id) !== -1) {
return {
valid: false,
type: 'redundant',
error: (trans.error_already_excluded || '"{name}" is already in an exclude list.').replace('{name}', name)
};
}
return { valid: true };
},
/**
* Check for parent-child conflicts in tree entities
*/
checkTreeConflicts: function(id, name, section, data, includeIds, excludeIds, trans) {
// Need tree data for parent-child lookups
if (!this.treeFlatData) {
return { valid: true };
}
var parentId = data && data.parentId ? parseInt(data.parentId, 10) : null;
// Build lookup for quick access
var lookup = {};
this.treeFlatData.forEach(function(item) {
lookup[parseInt(item.id, 10)] = item;
});
// Get all ancestor IDs
var ancestorIds = this.getAncestorIds(id, lookup);
// Get all descendant IDs
var descendantIds = this.getDescendantIds(id, lookup);
if (section === 'include') {
// Check if any ancestor is excluded
for (var i = 0; i < ancestorIds.length; i++) {
if (excludeIds.indexOf(ancestorIds[i]) !== -1) {
var ancestorName = lookup[ancestorIds[i]] ? lookup[ancestorIds[i]].name : 'Parent';
return {
valid: false,
type: 'parent_excluded',
error: (trans.error_parent_excluded || 'Cannot include "{name}" because its parent "{parent}" is excluded.').replace('{name}', name).replace('{parent}', ancestorName)
};
}
}
// Check if any descendant is excluded
for (var j = 0; j < descendantIds.length; j++) {
if (excludeIds.indexOf(descendantIds[j]) !== -1) {
var descendantName = lookup[descendantIds[j]] ? lookup[descendantIds[j]].name : 'Child';
return {
valid: false,
type: 'child_excluded',
error: (trans.error_child_excluded || 'Cannot include "{name}" because its child "{child}" is excluded. Remove the child from exclude first.').replace('{name}', name).replace('{child}', descendantName)
};
}
}
}
if (section === 'exclude') {
// Check if any ancestor is included
for (var k = 0; k < ancestorIds.length; k++) {
if (includeIds.indexOf(ancestorIds[k]) !== -1) {
var parentName = lookup[ancestorIds[k]] ? lookup[ancestorIds[k]].name : 'Parent';
return {
valid: false,
type: 'parent_included',
error: (trans.error_parent_included || 'Cannot exclude "{name}" because its parent "{parent}" is included. This would create a contradiction.').replace('{name}', name).replace('{parent}', parentName)
};
}
}
// Check if any descendant is included (warning about implicit exclusion)
var includedDescendants = [];
for (var m = 0; m < descendantIds.length; m++) {
if (includeIds.indexOf(descendantIds[m]) !== -1) {
var childName = lookup[descendantIds[m]] ? lookup[descendantIds[m]].name : 'Child';
includedDescendants.push(childName);
}
}
if (includedDescendants.length > 0) {
return {
valid: false,
type: 'children_included',
error: (trans.error_children_included || 'Cannot exclude "{name}" because its children ({children}) are included. Remove them from include first.').replace('{name}', name).replace('{children}', includedDescendants.slice(0, 3).join(', ') + (includedDescendants.length > 3 ? '...' : ''))
};
}
}
return { valid: true };
},
/**
* Get all ancestor IDs for a given item
*/
getAncestorIds: function(id, lookup) {
var ancestors = [];
var current = lookup[id];
while (current && current.parent_id) {
var parentId = parseInt(current.parent_id, 10);
if (parentId && lookup[parentId]) {
ancestors.push(parentId);
current = lookup[parentId];
} else {
break;
}
}
return ancestors;
},
/**
* Get all descendant IDs for a given item
*/
getDescendantIds: function(id, lookup) {
var descendants = [];
var self = this;
// Find direct children
Object.keys(lookup).forEach(function(key) {
var item = lookup[key];
if (parseInt(item.parent_id, 10) === id) {
var childId = parseInt(item.id, 10);
descendants.push(childId);
// Recursively get children's descendants
var childDescendants = self.getDescendantIds(childId, lookup);
descendants = descendants.concat(childDescendants);
}
});
return descendants;
},
/**
* Get chip IDs from a picker
*/
getChipIds: function($picker) {
var ids = [];
$picker.find('.entity-chip').each(function() {
ids.push(parseInt($(this).data('id'), 10));
});
return ids;
},
/**
* Validate pending selections (for tree view bulk operations)
* Returns array of invalid items
*/
validatePendingSelections: function(pendingSelections, section) {
var self = this;
var errors = [];
if (!pendingSelections || !pendingSelections.length) {
return errors;
}
pendingSelections.forEach(function(sel) {
var result = self.validateSelection(sel.id, sel.name, section, sel.data || {});
if (!result.valid) {
errors.push({
id: sel.id,
name: sel.name,
error: result.error,
type: result.type
});
}
});
return errors;
},
/**
* Show validation error toast
*/
showValidationError: function(message) {
var trans = this.config.trans || {};
var title = trans.validation_error || 'Selection Conflict';
// Remove existing toast
$('.es-validation-toast').remove();
// Create toast HTML
var html = '<div class="es-validation-toast">';
html += '<div class="es-toast-icon">' + this.esIcon('warning') + '</div>';
html += '<div class="es-toast-content">';
html += '<div class="es-toast-title">' + this.escapeHtml(title) + '</div>';
html += '<div class="es-toast-message">' + this.escapeHtml(message) + '</div>';
html += '</div>';
html += '<button type="button" class="es-toast-close">' + this.esIcon('close') + '</button>';
html += '</div>';
var $toast = $(html);
$('body').append($toast);
// Position near dropdown if visible
if (this.$dropdown && this.$dropdown.hasClass('show')) {
var dropdownOffset = this.$dropdown.offset();
$toast.css({
position: 'fixed',
top: dropdownOffset.top - $toast.outerHeight() - 10,
left: dropdownOffset.left,
zIndex: 10001
});
} else {
$toast.css({
position: 'fixed',
top: 20,
right: 20,
zIndex: 10001
});
}
// Animate in
$toast.hide().fadeIn(200);
// Auto-dismiss after 5 seconds
setTimeout(function() {
$toast.fadeOut(200, function() {
$(this).remove();
});
}, 5000);
// Close button
$toast.on('click', '.es-toast-close', function() {
$toast.fadeOut(200, function() {
$(this).remove();
});
});
},
/**
* Validate and add selection (wrapper that validates before adding)
* Returns true if added successfully, false if validation failed
*/
validateAndAddSelection: function($picker, id, name, data, section) {
var result = this.validateSelection(id, name, section, data || {});
if (!result.valid) {
this.showValidationError(result.error);
return false;
}
// Validation passed, add the selection
this.addSelection($picker, id, name, data);
return true;
}
};
})(jQuery);

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1 @@
// Placeholder - _core.js

View File

@@ -0,0 +1 @@
// Placeholder - _timeline.js

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1,351 @@
/**
* Entity Selector Mixins
* Reusable patterns - prefer Bootstrap utilities in HTML where possible
*/
@use "sass:color";
@use 'variables' as *;
// =============================================================================
// Layout
// =============================================================================
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin grid-columns($cols) {
display: grid;
grid-template-columns: repeat($cols, 1fr);
gap: $es-spacing-sm;
}
// =============================================================================
// Text
// =============================================================================
@mixin text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// =============================================================================
// Interactive Elements
// =============================================================================
// Reset button styles
@mixin button-reset {
padding: 0;
margin: 0;
background: none;
border: none;
cursor: pointer;
font: inherit;
color: inherit;
&:focus {
outline: none;
}
}
// Focus ring (Bootstrap 4 style)
@mixin focus-ring($color: $es-primary) {
outline: 0;
box-shadow: 0 0 0 0.2rem rgba($color, 0.25);
}
// Interactive hover state
@mixin interactive-item {
cursor: pointer;
transition: background-color $es-transition-fast, color $es-transition-fast;
&:hover {
background-color: $es-bg-hover;
}
}
// =============================================================================
// Cards & Containers
// =============================================================================
@mixin card {
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
}
@mixin dropdown-container {
position: absolute;
z-index: $es-z-dropdown;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
box-shadow: $es-shadow-lg;
}
// =============================================================================
// Form Elements
// =============================================================================
// Reset input styles (for inputs in custom wrappers)
@mixin input-reset {
padding: 0;
margin: 0;
background: none;
border: none;
font: inherit;
color: inherit;
&:focus {
outline: none;
}
}
@mixin input-base {
width: 100%;
padding: $es-spacing-sm $es-spacing-md;
font-size: $es-font-size-sm;
line-height: $es-line-height-normal;
color: $es-text-primary;
background-color: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-md;
transition: border-color $es-transition-fast, box-shadow $es-transition-fast;
&:focus {
border-color: $es-primary;
@include focus-ring($es-primary);
}
&::placeholder {
color: $es-text-light;
}
}
// =============================================================================
// Scrollbar
// =============================================================================
@mixin custom-scrollbar {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: $es-gray-100;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: $es-gray-300;
border-radius: 3px;
&:hover {
background: $es-gray-400;
}
}
}
// =============================================================================
// Badges & Chips
// =============================================================================
@mixin badge($bg: $es-gray-200, $color: $es-gray-700) {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.125rem 0.5rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
line-height: 1;
background-color: $bg;
color: $color;
border-radius: $es-radius-full;
}
// Count badge with preview icon (used for tab badges, match counts, totals)
// Note: Eye icon is provided in HTML via <i class="material-icons">visibility</i>
@mixin count-badge($bg: $es-primary) {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
min-width: 20px;
height: 20px;
padding: 0 0.5rem;
background: $bg;
color: $es-white;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
border-radius: $es-radius-full;
cursor: pointer;
transition: all $es-transition-fast;
flex-shrink: 0;
&:hover {
transform: scale(1.05);
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;
i {
font-size: 10px;
animation: spin 0.6s linear infinite;
}
&:hover {
transform: none;
box-shadow: none;
}
}
// Inactive/empty state
&.inactive,
&.no-matches {
background: $es-slate-400;
cursor: default;
&:hover {
transform: none;
box-shadow: none;
}
}
// Popover open state
&.popover-open {
background: color.adjust($bg, $lightness: -10%);
box-shadow: 0 2px 8px rgba($bg, 0.4);
}
// Icon inside badge (eye, spinner, etc.)
i {
font-size: 10px;
line-height: 1;
opacity: 0.8;
}
&:hover i {
opacity: 1;
}
.preview-count {
font-weight: $es-font-weight-bold;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-pulse {
0% { transform: rotate(0deg); opacity: 1; }
50% { opacity: 0.4; }
100% { transform: rotate(360deg); opacity: 1; }
}
// Global spin utility classes (Material Icons replacement for icon-spin / icon-spin-pulse)
.es-spin {
animation: spin 1s linear infinite;
}
.es-spin-pulse {
animation: spin-pulse 1s ease-in-out infinite;
}
@mixin chip {
display: inline-flex;
align-items: center;
gap: $es-spacing-xs;
padding: $es-spacing-xs $es-spacing-sm;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
background: $es-gray-200;
color: $es-gray-700;
border-radius: $es-radius-full;
.chip-remove {
@include button-reset;
@include flex-center;
width: 14px;
height: 14px;
font-size: 10px;
color: $es-text-muted;
border-radius: 50%;
&:hover {
background: rgba(0, 0, 0, 0.1);
color: $es-danger;
}
}
}
// =============================================================================
// Toggle Switch
// =============================================================================
@mixin toggle-switch($width: 36px, $height: 20px) {
position: relative;
width: $width;
height: $height;
border-radius: $height;
background: $es-gray-400;
transition: background-color $es-transition-normal;
cursor: pointer;
&::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: $height - 4px;
height: $height - 4px;
background: $es-white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: transform $es-transition-normal;
}
&.active {
background: $es-success;
&::after {
transform: translateX($width - $height);
}
}
}
// =============================================================================
// Screen Reader Only
// =============================================================================
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

View File

@@ -0,0 +1,154 @@
/**
* 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
// =============================================================================
$es-white: #ffffff !default;
$es-black: #000000 !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 (from prestashop-admin)
$es-success: admin.$success !default;
$es-success-light: #d4edda !default;
$es-success-dark: #1e7e34 !default;
$es-danger: admin.$danger !default;
$es-danger-light: #f8d7da !default;
$es-danger-dark: #bd2130 !default;
$es-warning: admin.$warning !default;
$es-warning-light: #fff3cd !default;
$es-info: admin.$info !default;
$es-info-light: #d1ecf1 !default;
// =============================================================================
// Gray Scale (Bootstrap 4)
// =============================================================================
$es-gray-100: admin.$light !default;
$es-gray-200: #e9ecef !default;
$es-gray-300: admin.$border-color !default;
$es-gray-400: #ced4da !default;
$es-gray-500: #adb5bd !default;
$es-gray-600: admin.$secondary !default;
$es-gray-700: #495057 !default;
$es-gray-800: admin.$dark !default;
$es-gray-900: #212529 !default;
// Slate (subtle variations)
$es-slate-50: #f8fafc !default;
$es-slate-100: #f1f5f9 !default;
$es-slate-200: #e2e8f0 !default;
$es-slate-300: #cbd5e1 !default;
$es-slate-400: #94a3b8 !default;
$es-slate-500: #64748b !default;
$es-slate-600: #475569 !default;
$es-slate-700: #334155 !default;
$es-slate-800: #1e293b !default;
$es-slate-900: #0f172a !default;
// Cyan
$es-cyan-50: #ecfeff !default;
$es-cyan-100: #cffafe !default;
$es-cyan-200: #a5f3fc !default;
$es-cyan-500: #06b6d4 !default;
$es-cyan-600: #0891b2 !default;
$es-cyan-700: #0e7490 !default;
// =============================================================================
// Semantic Aliases
// =============================================================================
$es-bg-header: $es-gray-100 !default;
$es-bg-hover: $es-gray-200 !default;
$es-bg-active: $es-gray-200 !default;
$es-bg-body: $es-white !default;
$es-border-color: admin.$border-color !default;
$es-border-light: $es-gray-200 !default;
$es-border-dark: $es-gray-400 !default;
$es-text-primary: $es-gray-900 !default;
$es-text-secondary: $es-gray-700 !default;
$es-text-muted: $es-gray-600 !default;
$es-text-light: $es-gray-500 !default;
// =============================================================================
// Spacing (Bootstrap 4 compatible, derived from admin.$spacer)
// =============================================================================
$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 (from prestashop-admin)
// =============================================================================
$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;
// =============================================================================
// Box Shadows (Bootstrap 4 compatible)
// =============================================================================
$es-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !default;
$es-shadow-md: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !default;
$es-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175) !default;
$es-shadow-xl: 0 1.5rem 4rem rgba(0, 0, 0, 0.2) !default;
// =============================================================================
// Transitions
// =============================================================================
$es-transition-fast: 0.15s ease-in-out !default;
$es-transition-normal: 0.2s ease-in-out !default;
$es-transition-slow: 0.3s ease-in-out !default;
// =============================================================================
// Z-Index (below Bootstrap modal)
// =============================================================================
$es-z-dropdown: 1000 !default;
$es-z-modal: 1050 !default;
$es-z-popover: 1060 !default;
$es-z-tooltip: 1070 !default;
// =============================================================================
// Typography
// =============================================================================
$es-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
$es-font-size-xs: 0.75rem !default; // 12px
$es-font-size-sm: 0.875rem !default; // 14px
$es-font-size-base: 1rem !default; // 16px
$es-font-size-lg: 1.125rem !default; // 18px
$es-font-weight-normal: 400 !default;
$es-font-weight-medium: 500 !default;
$es-font-weight-semibold: 600 !default;
$es-font-weight-bold: 700 !default;
$es-line-height-tight: 1.25 !default;
$es-line-height-normal: 1.5 !default;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,396 @@
/**
* Combination Attributes Picker Component
* Product attribute combination selection styles
*/
@use "sass:color";
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Main container
.combination-attributes-picker {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
// Mode toggle (Any/All)
.combination-mode-toggle {
display: inline-flex;
gap: 0.25rem;
padding: 0.125rem;
background: $es-slate-100;
border-radius: $es-radius-md;
margin-bottom: 0.5rem;
}
.combination-mode-option {
display: flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
font-size: 11px;
color: $es-text-muted;
padding: 0.25rem 0.625rem;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
input[type="radio"] {
display: none;
}
.mode-label {
user-select: none;
}
&:hover {
color: $es-primary;
background: rgba($es-primary, 0.1);
}
&:has(input[type="radio"]:checked) {
background: $es-primary;
color: $es-white;
font-weight: $es-font-weight-medium;
}
}
// Groups container
.combination-groups-container {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-md;
}
// Loading/Empty/Error states
.combination-loading,
.combination-empty,
.combination-error {
color: $es-text-muted;
font-style: italic;
padding: 0.5rem;
}
.combination-error {
color: $es-danger;
}
// Section header
.combinations-section {
margin-bottom: $es-spacing-md;
}
.combinations-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $es-spacing-sm;
}
.combinations-label {
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-muted;
}
.combinations-help {
font-size: 11px;
color: $es-slate-400;
}
// Toggle combinations button
.btn-toggle-combinations {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
color: $es-primary;
background: transparent;
border: 1px solid $es-primary;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
}
.btn-remove-combinations {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
color: $es-danger;
background: transparent;
&:hover {
text-decoration: underline;
}
}
// =============================================================================
// Attribute Group
// =============================================================================
.comb-attr-group {
flex: none;
min-width: 120px;
max-width: 200px;
background: $es-white;
border: 1px solid $es-gray-300;
border-radius: $es-radius-sm;
overflow: hidden;
&.has-selections {
border-color: $es-primary;
}
}
.comb-attr-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.375rem 0.625rem;
background: $es-slate-100;
border-bottom: 1px solid $es-gray-300;
font-weight: $es-font-weight-semibold;
font-size: $es-font-size-xs;
color: $es-slate-800;
.comb-attr-group.has-selections & {
background: $es-cyan-50;
border-bottom-color: $es-cyan-200;
}
}
.comb-attr-group-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comb-attr-group-count {
flex-shrink: 0;
min-width: 18px;
height: 18px;
padding: 0 0.25rem;
background: $es-gray-300;
border-radius: $es-radius-full;
font-size: 11px;
font-weight: $es-font-weight-semibold;
line-height: 18px;
text-align: center;
color: $es-text-muted;
.comb-attr-group.has-selections & {
background: $es-primary;
color: $es-white;
}
}
// Toolbar
.comb-attr-toolbar {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.375rem;
background: $es-slate-50;
border-bottom: 1px solid $es-slate-100;
}
.comb-toolbar-btn {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
background: $es-white;
border: 1px solid $es-gray-300;
border-radius: $es-radius-sm;
color: $es-text-muted;
cursor: pointer;
font-size: $es-font-size-xs;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-100;
border-color: $es-slate-400;
color: $es-slate-800;
}
}
.comb-attr-search {
flex: 1;
min-width: 60px;
padding: 0.125rem 0.375rem;
border: 1px solid $es-gray-300;
border-radius: $es-radius-sm;
font-size: 11px;
outline: none;
&:focus {
border-color: $es-primary;
}
&::placeholder {
color: $es-slate-400;
}
}
// Values container
.comb-attr-values {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
padding: 0.375rem;
max-height: 150px;
overflow-y: auto;
@include custom-scrollbar;
}
.comb-attr-loading,
.comb-attr-empty,
.comb-attr-error {
width: 100%;
text-align: center;
color: $es-slate-400;
font-size: 11px;
padding: 0.25rem;
}
.comb-attr-error {
color: $es-danger;
}
// Individual value
.comb-attr-value {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
background: $es-white;
border: 1px solid $es-slate-400;
border-radius: 0.75rem;
font-size: 11px;
color: $es-slate-600;
cursor: pointer;
transition: all $es-transition-fast;
white-space: nowrap;
&:hover {
background: $es-slate-100;
border-color: $es-text-muted;
}
&.selected {
background: $es-primary;
border-color: $es-primary-hover;
color: $es-white;
&:hover {
background: $es-primary-hover;
border-color: color.adjust($es-primary-hover, $lightness: -5%);
}
}
}
.comb-attr-value-count {
font-size: 9px;
color: $es-slate-400;
background: $es-slate-100;
padding: 1px 0.25rem;
border-radius: 0.5rem;
min-width: 14px;
text-align: center;
.comb-attr-value.selected & {
color: $es-white;
background: rgba(255, 255, 255, 0.3);
}
}
// =============================================================================
// Combination Conditions (Row-based)
// =============================================================================
.combination-conditions-container {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.combination-condition-row {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm;
background: $es-slate-50;
border-radius: $es-radius-sm;
}
.combination-group-select,
.combination-values-select {
flex: 1;
min-width: 120px;
}
.combination-equals {
font-size: $es-font-size-xs;
color: $es-text-muted;
padding: 0 0.25rem;
}
.btn-add-combination-condition {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-primary;
background: transparent;
border: 1px dashed $es-primary;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
i {
font-size: 10px;
}
}
.btn-remove-combination-row {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: rgba($es-danger, 0.1);
color: $es-danger;
}
i {
font-size: 12px;
}
}
}

View File

@@ -0,0 +1,358 @@
/**
* Condition Trait Base Styles
* Shared styling for all condition trait components
*/
@use '../variables' as *;
@use '../mixins' as *;
// Base condition trait container
.condition-trait {
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
margin-bottom: $es-spacing-lg;
&:last-child {
margin-bottom: 0;
}
}
// Collapsed state
.condition-trait.collapsed {
.condition-trait-header {
border-bottom-color: transparent;
border-radius: $es-radius-lg;
}
.collapse-icon {
transform: rotate(180deg);
}
}
// =============================================================================
// Trait Header
// =============================================================================
.condition-trait-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-lg;
flex-wrap: wrap;
padding: 0.875rem $es-spacing-lg;
background: $es-slate-50;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
cursor: pointer;
transition: background-color $es-transition-fast;
&:hover {
background: $es-slate-100;
}
}
.trait-header-left {
display: flex;
align-items: center;
gap: $es-spacing-md;
min-width: 0;
flex: 1;
}
.trait-icon {
font-size: 1.125rem;
color: $es-text-muted;
flex-shrink: 0;
}
.trait-title-group {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.trait-title {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-slate-800;
white-space: nowrap;
}
.trait-subtitle {
font-size: $es-font-size-xs;
color: $es-text-muted;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Schedule summary (shows current config at a glance)
.trait-summary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.625rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-primary;
background: rgba($es-primary, 0.08);
border-radius: $es-radius-full;
white-space: nowrap;
margin-left: $es-spacing-md;
flex-shrink: 0;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
&:empty {
display: none;
}
}
.trait-header-right {
display: flex;
align-items: center;
gap: $es-spacing-md;
flex-shrink: 0;
margin-left: auto;
}
.trait-header-actions {
display: flex;
align-items: center;
gap: $es-spacing-sm;
}
// Collapse icon
.collapse-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
font-size: $es-font-size-sm;
color: $es-text-muted;
cursor: pointer;
transition: all 0.2s;
border-radius: $es-radius-sm;
background: transparent;
&:hover {
color: $es-primary;
background: rgba($es-primary, 0.08);
}
}
// Show all toggle
.trait-show-all-toggle {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: $es-font-size-xs;
color: $es-primary;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
// Trait total count badge (global fallback)
.trait-total-count {
@include count-badge($es-primary);
}
// Required indicator
.trait-required {
color: $es-danger;
font-size: $es-font-size-xs;
}
// Validation error
.trait-validation-error {
color: $es-danger;
font-size: $es-font-size-xs;
margin-top: 0.25rem;
}
// Trait toggle button
.trait-toggle {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
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-md;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-50;
border-color: $es-gray-300;
}
&.active {
color: $es-primary;
border-color: $es-primary;
background: $es-primary-light;
}
}
// =============================================================================
// Trait Body
// =============================================================================
.condition-trait-body {
padding: $es-spacing-lg;
border-radius: 0 0 $es-radius-lg $es-radius-lg;
background: $es-white;
animation: slideDown 0.2s ease-out;
}
// Condition trait collapsed - hide body
.condition-trait.collapsed .condition-trait-body {
display: none;
}
// =============================================================================
// Section Styles
// =============================================================================
.schedule-section,
.context-section {
margin-bottom: 1.25rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid $es-slate-100;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: 0;
}
}
.section-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 13px;
font-weight: $es-font-weight-semibold;
color: $es-slate-600;
margin-bottom: $es-spacing-md;
i {
font-size: $es-font-size-sm;
color: $es-slate-400;
margin-right: 0.25rem;
}
}
.section-content {
// Container for section content
}
.section-hint {
margin-top: 0.5rem;
font-size: 11px;
color: $es-slate-400;
}
// =============================================================================
// Full-width Form Group Override
// =============================================================================
.form-group.condition-trait-fullwidth {
display: block !important;
> .control-label {
display: none !important;
}
> .col-lg-8,
> .col-lg-8.col-lg-offset-3 {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
padding-left: $es-spacing-lg !important;
padding-right: $es-spacing-lg !important;
margin: 0 !important;
margin-left: 0 !important;
}
}
// Condition traits group label
.condition-traits-group-label {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-slate-700;
margin-bottom: $es-spacing-md;
}
.condition-traits-wrapper {
display: flex;
flex-direction: column;
gap: $es-spacing-md;
}
// =============================================================================
// Collapse Header (form-content layout)
// =============================================================================
.entity-selector-collapse-header {
padding: 0;
margin-bottom: $es-spacing-sm;
.btn-collapse-toggle {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0;
background: none;
border: none;
color: $es-primary;
font-size: $es-font-size-sm;
cursor: pointer;
transition: color $es-transition-fast;
&:hover {
color: $es-primary-hover;
}
.collapse-icon {
font-size: 1.25rem;
transition: transform 0.2s;
}
.collapse-label {
font-weight: $es-font-weight-medium;
}
}
}
// When collapsed, rotate icon
.condition-trait.collapsed .entity-selector-collapse-header {
.collapse-icon {
// Icon already shows expand_more when collapsed
}
}
// =============================================================================
// Animations
// =============================================================================
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,516 @@
/**
* Entity Item - Shared Base Component
* Unified styling for entity items in chips, lists, and previews
*
* Variants:
* - .entity-item (base) - default list-row style
* - .entity-item.chip-style - pill/chip style (compact)
* - .entity-item.card-style - card/grid style
*/
@use '../variables' as *;
@use '../mixins' as *;
// =============================================================================
// Entity Item Sizing
// =============================================================================
$entity-item-image-sm: 20px;
$entity-item-image-md: 32px;
$entity-item-image-lg: 48px;
// =============================================================================
// Base Entity Item (list-row layout)
// =============================================================================
.entity-item {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm;
background: $es-white;
border-radius: $es-radius-sm;
transition: background $es-transition-fast;
&:hover {
background: $es-bg-hover;
}
// Clickable variant
&.clickable {
cursor: pointer;
}
// Selected state
&.selected {
background: $es-primary-light;
}
}
// -----------------------------------------------------------------------------
// Entity Item Image
// -----------------------------------------------------------------------------
.entity-item-image {
flex-shrink: 0;
width: $entity-item-image-md;
height: $entity-item-image-md;
object-fit: cover;
border-radius: $es-radius-sm;
background: $es-slate-100;
}
// Size variants
.entity-item-image--sm {
width: $entity-item-image-sm;
height: $entity-item-image-sm;
border-radius: 50%;
}
.entity-item-image--lg {
width: $entity-item-image-lg;
height: $entity-item-image-lg;
}
// No-image placeholder
.entity-item-no-image {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: $entity-item-image-md;
height: $entity-item-image-md;
background: $es-slate-100;
color: $es-text-muted;
border-radius: $es-radius-sm;
font-size: $es-font-size-sm;
&--sm {
width: $entity-item-image-sm;
height: $entity-item-image-sm;
font-size: 10px;
border-radius: 50%;
}
}
// -----------------------------------------------------------------------------
// Entity Item Info (name + meta)
// -----------------------------------------------------------------------------
.entity-item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.entity-item-name {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
color: $es-text-primary;
@include text-truncate;
}
.entity-item-meta {
font-size: $es-font-size-xs;
color: $es-text-muted;
@include text-truncate;
}
// -----------------------------------------------------------------------------
// Entity Item Badge/Price (right side)
// -----------------------------------------------------------------------------
.entity-item-badge {
flex-shrink: 0;
padding: 0.125rem 0.5rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
background: $es-slate-100;
color: $es-text-muted;
border-radius: $es-radius-sm;
}
.entity-item-price {
flex-shrink: 0;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-primary;
}
// -----------------------------------------------------------------------------
// Entity Item Actions (remove button, etc.)
// -----------------------------------------------------------------------------
.entity-item-action {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 20px;
height: 20px;
color: $es-text-muted;
border-radius: 50%;
transition: all $es-transition-fast;
&:hover {
background: rgba(0, 0, 0, 0.1);
color: $es-danger;
}
i {
font-size: 10px;
}
}
// =============================================================================
// Chip Style Variant (compact pill)
// =============================================================================
.entity-item.chip-style {
display: inline-flex;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: $es-slate-100;
border-radius: $es-radius-full;
&:hover {
background: $es-slate-200;
}
.entity-item-image {
width: $entity-item-image-sm;
height: $entity-item-image-sm;
border-radius: 50%;
}
.entity-item-no-image {
width: $entity-item-image-sm;
height: $entity-item-image-sm;
font-size: 10px;
border-radius: 50%;
}
.entity-item-info {
flex-direction: row;
align-items: center;
gap: 0.25rem;
}
.entity-item-name {
font-size: $es-font-size-xs;
}
.entity-item-meta {
display: none;
}
.entity-item-action {
width: 16px;
height: 16px;
margin-left: 0.125rem;
}
}
// =============================================================================
// List Style Variant (bordered rows)
// =============================================================================
.entity-item.list-style {
padding: $es-spacing-sm 0;
background: transparent;
border-bottom: 1px solid $es-border-color;
border-radius: 0;
&:last-child {
border-bottom: none;
}
&:hover {
background: $es-bg-hover;
}
}
// =============================================================================
// Entity Item Container (wrapper for multiple items)
// =============================================================================
.entity-items-container {
display: flex;
flex-direction: column;
background: $es-slate-50;
border: 1px solid $es-border-color;
border-radius: $es-radius-md;
overflow: hidden;
}
// Toolbar (filter, sort, count, clear)
.entity-items-toolbar {
display: none;
align-items: center;
flex-wrap: nowrap;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
padding-bottom: 0;
background: transparent;
&.has-items {
display: flex;
}
}
// Filter input
.entity-items-filter {
all: unset;
display: block;
flex: 1 1 auto;
min-width: 80px;
width: auto;
height: auto;
padding: 0.2rem 0.5rem 0.2rem 1.5rem;
background: $es-white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") no-repeat 0.375rem center;
background-size: 10px;
border: 1px solid $es-slate-300;
border-radius: $es-radius-sm;
font-size: 11px;
line-height: 1.4;
color: $es-text-primary;
box-sizing: border-box;
transition: all $es-transition-fast;
&::placeholder {
color: $es-text-muted;
font-size: 11px;
}
&:focus {
outline: none;
border-color: $es-primary;
box-shadow: 0 0 0 2px rgba($es-primary, 0.1);
}
}
// Sort dropdown
.entity-items-sort {
all: unset;
flex: 0 0 auto;
padding: 0.2rem 1.25rem 0.2rem 0.5rem;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
background: $es-white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23666' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E") no-repeat right 0.375rem center;
background-size: 8px;
font-size: 10px;
line-height: 1.4;
color: $es-text-secondary;
cursor: pointer;
box-sizing: border-box;
white-space: nowrap;
&:hover {
border-color: $es-primary;
}
&:focus {
outline: none;
border-color: $es-primary;
box-shadow: 0 0 0 2px rgba($es-primary, 0.1);
}
}
// Count badge
.entity-items-count {
display: inline-flex;
align-items: center;
flex-shrink: 0;
gap: 0.125rem;
padding: 0.2rem 0.5rem;
background: $es-slate-200;
color: $es-text-secondary;
font-size: 10px;
font-weight: $es-font-weight-semibold;
border-radius: $es-radius-sm;
white-space: nowrap;
line-height: 1.4;
&.has-filter {
background: $es-cyan-100;
color: $es-cyan-700;
}
}
// Clear button
.entity-items-clear {
@include button-reset;
display: inline-flex;
align-items: center;
flex-shrink: 0;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
color: $es-danger;
font-size: 10px;
font-weight: $es-font-weight-medium;
background: rgba($es-danger, 0.1);
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
white-space: nowrap;
line-height: 1.4;
&:hover {
background: $es-danger;
color: $es-white;
}
i {
font-size: 9px;
flex-shrink: 0;
}
}
// Items list area
.entity-items-list {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
padding: $es-spacing-sm $es-spacing-md $es-spacing-md;
min-height: 40px;
max-height: 300px;
overflow-y: auto;
@include custom-scrollbar;
&:empty {
display: none;
}
// List layout (vertical)
&.list-layout {
flex-direction: column;
flex-wrap: nowrap;
gap: 0;
padding: $es-spacing-sm;
}
}
// Load more section
.entity-items-load-more {
display: flex;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: transparent;
border-top: 1px dashed $es-border-color;
.load-more-label {
font-size: $es-font-size-xs;
color: $es-text-muted;
}
.load-more-select {
appearance: none;
padding: 0.25rem 1.75rem 0.25rem 0.5rem;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
background: $es-white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23666' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E") no-repeat right 0.5rem center;
background-size: 8px;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-primary;
cursor: pointer;
transition: all $es-transition-fast;
height: auto;
min-height: 0;
line-height: 1.3;
&:hover {
border-color: $es-primary;
background-color: $es-primary-light;
}
&:focus {
outline: none;
border-color: $es-primary;
box-shadow: 0 0 0 2px rgba($es-primary, 0.1);
}
}
.load-more-remaining {
font-size: $es-font-size-xs;
color: $es-text-muted;
.remaining-count {
font-weight: $es-font-weight-semibold;
color: $es-text-secondary;
}
}
.btn-load-more {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-xs;
margin: 0;
border: none;
color: $es-primary;
background: $es-primary-light;
border-radius: $es-radius-sm;
cursor: pointer;
transition: all $es-transition-fast;
font: inherit;
i {
font-size: 14px;
}
&:hover {
background: rgba($es-primary, 0.2);
}
&.loading {
cursor: wait;
i {
animation: spin 0.6s linear infinite;
}
}
}
}
// =============================================================================
// Empty & Loading States
// =============================================================================
.entity-items-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-xl;
text-align: center;
color: $es-text-muted;
i {
font-size: 2rem;
opacity: 0.5;
}
p {
margin: 0;
font-size: $es-font-size-sm;
}
}
.entity-items-loading {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-xl;
color: $es-text-muted;
i {
font-size: 20px;
animation: spin 0.6s linear infinite;
}
}

View File

@@ -0,0 +1,450 @@
/**
* Entity Selector - Main Component Styles
* Wrapper, header, body, tabs, blocks
*/
@use '../variables' as *;
@use '../mixins' as *;
// Main wrapper (supports both .target-conditions-trait and .entity-selector-trait)
.target-conditions-trait,
.entity-selector-trait {
position: relative;
overflow: visible;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
// Base Material Icons sizing — 18px throughout entity-selector
// !important needed to override .bootstrap .material-icons:not(.js-mobile-menu) { font-size: inherit }
.material-icons {
font-size: 18px !important;
}
// Trait Header (collapsible)
.condition-trait-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: $es-spacing-md;
padding: 0.875rem $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
cursor: pointer;
user-select: none;
transition: background-color $es-transition-fast;
&:hover {
background: $es-bg-hover;
}
}
.trait-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
flex: 1;
}
.trait-icon {
font-size: $es-font-size-lg;
color: $es-text-muted;
flex-shrink: 0;
}
.trait-title-group {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.trait-title {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
white-space: nowrap;
}
.trait-subtitle {
font-size: $es-font-size-xs;
color: $es-text-muted;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Total count badge in header
.trait-total-count {
@include count-badge($es-primary);
margin-left: $es-spacing-sm;
}
// Show all toggle switch
.trait-show-all-toggle {
display: inline-flex;
align-items: center;
gap: $es-spacing-sm;
margin-right: 0.75rem;
padding: 0.25rem $es-spacing-sm;
border-radius: $es-radius-sm;
cursor: pointer;
user-select: none;
transition: background-color $es-transition-fast;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
.toggle-label {
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-muted;
}
.show-all-checkbox {
display: none;
}
.toggle-slider {
position: relative;
width: 36px;
height: 20px;
background: $es-slate-300;
border-radius: $es-radius-full;
transition: background-color $es-transition-normal;
&::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: $es-white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: transform $es-transition-normal;
}
}
.show-all-checkbox:checked + .toggle-slider {
background: $es-success;
&::after {
transform: translateX(16px);
}
}
}
// Validation error states
&.has-validation-error {
border-color: $es-danger;
box-shadow: 0 0 0 3px rgba($es-danger, 0.1);
.condition-trait-header {
border-bottom-color: $es-danger;
}
}
.trait-validation-error {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: 0.625rem $es-spacing-md;
background: $es-danger-light;
color: #b91c1c;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
border-bottom: 1px solid #fecaca;
i {
color: $es-danger;
}
}
// Required indicator
&.trait-required .trait-title::after {
content: ' *';
color: $es-danger;
}
// Body
.condition-trait-body {
padding: 0;
background: $es-white;
border-radius: 0 0 $es-radius-lg $es-radius-lg;
}
// Block type tabs
.target-block-tabs {
display: flex;
flex-wrap: wrap;
gap: 0;
padding: 0;
background: $es-slate-100;
border-bottom: 1px solid $es-border-color;
}
.target-block-tab {
position: relative;
display: flex;
align-items: center;
gap: $es-spacing-sm;
flex: none;
min-width: 0;
padding: 0.625rem $es-spacing-md;
margin-bottom: -1px;
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
color: $es-text-muted;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-slate-700;
}
&.active {
background: $es-white;
border-bottom-color: $es-cyan-500;
color: $es-primary;
}
i.material-icons {
font-size: 18px !important;
}
.tab-label {
white-space: nowrap;
}
.tab-badge {
@include count-badge($es-primary);
}
&.has-data:not(.active) .tab-badge {
@include count-badge($es-slate-400);
}
}
// Tabs row with actions (form-content layout)
.entity-selector-tabs-row {
display: flex;
align-items: stretch;
background: $es-slate-100;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
.target-block-tabs {
flex: 1;
border-bottom: 0;
border-radius: $es-radius-lg 0 0 0;
}
}
// Expand/collapse groups button (standalone, in tabs row)
.entity-selector-actions:not(.btn-toggle-blocks) {
display: flex;
align-items: center;
padding: $es-spacing-xs $es-spacing-md;
border-left: 1px solid $es-border-color;
.btn-toggle-groups {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
background: $es-white;
border: 1px solid $es-slate-400;
border-radius: $es-radius-md;
color: $es-slate-700;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
color: $es-primary;
border-color: $es-primary;
}
.material-icons {
font-size: 18px !important;
}
}
}
// Expand/collapse toggle area (entire section is clickable)
.entity-selector-actions.btn-toggle-blocks {
display: flex;
align-items: center;
justify-content: center;
padding: 0 $es-spacing-md;
background: $es-slate-100;
border-left: 1px solid $es-border-color;
color: $es-slate-400;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-primary;
}
.material-icons {
font-size: 20px !important;
}
}
// When expanded - highlight the toggle area
.entity-selector-trait:not(.blocks-collapsed) .entity-selector-actions.btn-toggle-blocks {
background: $es-primary-light;
border-left-color: $es-primary;
color: $es-primary;
}
// Blocks content wrapper (for form-content layout collapse)
.entity-selector-blocks-content {
// Inherits styles from condition-trait-body context
}
// Block container
.target-block-container {
display: none;
&.active {
display: block;
}
}
.target-block-content {
padding: $es-spacing-md;
}
.target-block-groups {
display: flex;
flex-direction: column;
gap: $es-spacing-md;
}
// Block header (for standalone blocks)
.target-block-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;
}
// Empty state
.target-block-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-xl;
text-align: center;
color: $es-text-muted;
i {
font-size: 2rem !important;
opacity: 0.5;
}
p {
margin: 0;
font-size: $es-font-size-sm;
}
}
// Collapse toggle
.trait-collapse-toggle,
.collapse-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
cursor: pointer;
transition: transform $es-transition-normal;
&.collapsed {
transform: rotate(-90deg);
}
}
// Header actions
.trait-header-right {
display: flex;
align-items: center;
gap: $es-spacing-sm;
}
// Collapsed state
&.collapsed {
.condition-trait-body {
display: none;
}
.condition-trait-header {
border-radius: $es-radius-lg;
}
}
}
// Single mode specific styles
.target-conditions-trait.single-mode,
.entity-selector-trait.single-mode {
.target-block-tabs {
display: none;
}
.target-block-container {
display: block;
}
}
// Header action buttons
.target-conditions-trait,
.entity-selector-trait {
.header-actions {
display: flex;
align-items: center;
gap: $es-spacing-xs;
}
.header-action-btn {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem $es-spacing-sm;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-muted;
background: transparent;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i {
font-size: 14px !important;
}
}
}

View File

@@ -0,0 +1,982 @@
/**
* Groups Component
* Selection groups, include/exclude sections, method selectors
*/
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Group container
.target-group {
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
overflow: hidden;
}
// Group header
.target-group-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-md;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
}
.target-group-title {
display: flex;
align-items: center;
gap: $es-spacing-sm;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
.group-number {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 0.25rem;
background: $es-primary;
color: $es-white;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-bold;
border-radius: $es-radius-full;
}
}
.target-group-actions {
display: flex;
align-items: center;
gap: $es-spacing-xs;
}
.group-action-btn {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
&.danger:hover {
background: $es-danger-light;
color: $es-danger;
}
}
// Group body
.target-group-body,
.group-body {
padding: $es-spacing-md;
}
// Include section
.include-section {
margin-bottom: $es-spacing-md;
}
.section-label {
display: flex;
align-items: center;
gap: $es-spacing-xs;
margin-bottom: $es-spacing-sm;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
text-transform: uppercase;
letter-spacing: 0.05em;
&.label-include {
color: $es-success-dark;
i {
color: $es-success;
}
}
&.label-exclude {
color: $es-danger;
i {
color: $es-danger;
}
}
}
// Method selector
.method-selector {
display: flex;
align-items: center;
gap: $es-spacing-sm;
margin-bottom: $es-spacing-sm;
}
.method-selector-wrapper {
flex: 1;
position: relative;
}
.method-select {
@include input-base;
padding-right: 2rem;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
}
.method-help-btn {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: $es-text-muted;
border-radius: $es-radius-full;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-100;
color: $es-primary;
}
}
// Value picker (search trigger)
.value-picker {
position: relative;
}
.value-picker-trigger {
@include button-reset;
display: flex;
align-items: center;
gap: $es-spacing-sm;
width: 100%;
padding: $es-spacing-sm $es-spacing-md;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-md;
color: $es-text-muted;
font-size: $es-font-size-sm;
text-align: left;
transition: all $es-transition-fast;
&:hover {
border-color: $es-slate-300;
}
&:focus {
border-color: $es-primary;
@include focus-ring($es-primary);
}
i {
color: $es-text-light;
}
}
// Pattern input (text input for patterns)
.pattern-input-wrapper {
position: relative;
}
.pattern-input {
@include input-base;
font-family: monospace;
}
.pattern-add-btn {
@include button-reset;
position: absolute;
right: 0.25rem;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: $es-primary;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
}
// Multi-range input (price ranges)
.multi-range-container {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.range-row {
display: flex;
align-items: center;
gap: $es-spacing-sm;
}
.range-input {
@include input-base;
width: 100px;
text-align: center;
}
.range-separator {
color: $es-text-muted;
font-size: $es-font-size-sm;
}
.range-remove-btn {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-danger-light;
color: $es-danger;
}
}
.range-add-btn {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
color: $es-primary;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
}
// Multi-select tiles (stock status, etc.)
.multi-select-tiles {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
}
.multi-select-tile {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
background: $es-slate-100;
color: $es-text-secondary;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border: 1px solid transparent;
border-radius: $es-radius-full;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
}
&.selected {
background: $es-primary-light;
color: $es-primary;
border-color: $es-primary;
}
}
// Exclude section
.exclude-section {
margin-top: $es-spacing-md;
padding-top: $es-spacing-md;
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-content {
flex: 1;
}
.exclude-remove-btn {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
border-radius: $es-radius-sm;
flex-shrink: 0;
transition: all $es-transition-fast;
&:hover {
background: $es-danger-light;
color: $es-danger;
}
}
.add-exclude-btn {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-top: $es-spacing-sm;
padding: 0.25rem 0.5rem;
color: $es-danger;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border: 1px dashed $es-danger;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-danger-light;
}
}
// Add group button (used in block-footer)
.btn-add-group {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
color: $es-primary;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
background: rgba($es-primary, 0.05);
border: 1px dashed $es-primary;
border-radius: 0.375rem;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: rgba($es-primary, 0.1);
}
i {
font-size: 12px;
}
}
// Block footer
.block-footer {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-md;
border-top: 1px solid $es-border-color;
}
// Block body
.block-body {
padding: 0;
}
// Groups container
.groups-container {
padding: $es-spacing-md;
}
// Groups empty state
.groups-empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-xl;
color: $es-text-muted;
font-size: $es-font-size-sm;
}
// Selection group
.selection-group {
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
margin-bottom: $es-spacing-md;
&:last-child {
margin-bottom: 0;
}
&.collapsed {
.group-body {
display: none;
}
.group-collapse-toggle i {
transform: rotate(-90deg);
}
}
}
// Group header
.group-header {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
cursor: pointer;
&.group-header-single {
padding: $es-spacing-xs $es-spacing-md;
background: transparent;
border-bottom: none;
}
}
.group-collapse-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
color: $es-text-muted;
i {
font-size: 20px !important;
transition: transform $es-transition-fast;
}
}
.group-name-wrapper {
flex: 1;
display: flex;
align-items: center;
gap: $es-spacing-sm;
}
.group-name-input {
flex: 1;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
background: transparent;
border: 1px solid transparent;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover,
&:focus {
background: $es-white;
border-color: $es-border-color;
outline: none;
}
&::placeholder {
color: $es-text-muted;
font-weight: $es-font-weight-medium;
}
}
.group-count-badge {
@include count-badge($es-primary);
}
.btn-remove-group {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-danger-light;
color: $es-danger;
}
}
// 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 {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
// Method selector wrapper (from PHP)
.method-selector-wrapper {
display: flex;
align-items: center;
gap: $es-spacing-sm;
}
.method-info-placeholder {
display: flex;
align-items: center;
min-width: 20px;
}
.include-method-select,
.exclude-method-select {
flex: 1;
@include input-base;
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: 16px !important;
}
.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;
}
.except-separator {
display: flex;
align-items: center;
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.75rem;
background: $es-danger-light;
color: $es-danger;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
border-radius: $es-radius-full;
white-space: nowrap;
flex-shrink: 0;
i {
font-size: 12px !important;
}
}
.exclude-rows-container {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.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;
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 {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-danger-light;
color: $es-danger;
}
}
.btn-add-exclude,
.btn-add-another-exclude {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-top: $es-spacing-sm;
padding: 0.375rem 0.625rem;
color: $es-danger;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
background: transparent;
border: 1px dashed rgba($es-danger, 0.5);
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-danger-light;
border-color: $es-danger;
}
i {
font-size: 12px !important;
}
}
// Group modifiers (inline version from PHP)
// Uses negative margins to break out of .group-body padding
// Elements use .mpr-input-compact class to opt out of admin.css global sizing
.group-modifiers {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: $es-spacing-md;
padding: $es-spacing-sm $es-spacing-md;
margin: $es-spacing-md (-$es-spacing-md) (-$es-spacing-md);
background: $es-slate-50;
border-top: 1px solid $es-border-color;
border-radius: 0 0 $es-radius-lg $es-radius-lg;
}
.modifier-inline {
display: inline-flex;
align-items: center;
gap: 0.375rem;
flex-shrink: 0;
}
// Common height for all modifier controls
$modifier-height: 26px;
.group-modifier-limit {
width: 50px;
max-width: 50px;
min-width: 50px;
height: $modifier-height;
padding: 0 0.375rem;
font-size: $es-font-size-xs;
text-align: center;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
box-sizing: border-box;
&:focus {
border-color: $es-primary;
outline: none;
}
}
// 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 {
width: auto;
height: $modifier-height;
padding: 0 1.25rem 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;
box-sizing: border-box;
&: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: 14px !important;
}
}
}
// Fallback for elements outside .modifier-sort context
.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;
cursor: pointer;
&:focus {
border-color: $es-primary;
outline: none;
}
}
.btn-sort-dir {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: $modifier-height;
height: $modifier-height;
color: $es-text-muted;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-100;
color: $es-text-secondary;
}
}
.group-preview-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: $es-slate-100;
color: $es-text-muted;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-full;
transition: all $es-transition-fast;
&.clickable {
cursor: pointer;
&:hover {
background: $es-primary-light;
color: $es-primary;
}
}
}
// OR separator between groups
.group-separator {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-sm 0;
color: $es-text-muted;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
text-transform: uppercase;
letter-spacing: 0.1em;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: $es-border-color;
margin: 0 $es-spacing-md;
}
}
// Group modifiers (limit, sort)
.group-modifiers {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-md;
padding-top: $es-spacing-md;
margin-top: $es-spacing-md;
border-top: 1px solid $es-border-color;
}
.modifier-group {
display: flex;
align-items: center;
gap: $es-spacing-sm;
}
.modifier-label {
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-muted;
white-space: nowrap;
}
.modifier-input {
@include input-base;
width: 80px;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
}
.modifier-select {
@include input-base;
width: auto;
padding: 0.25rem 1.5rem 0.25rem 0.5rem;
font-size: $es-font-size-xs;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.25rem center;
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
// Condition match count badge
.condition-match-count {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background: $es-slate-100;
color: $es-text-muted;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-full;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
}
&.has-results {
background: $es-primary-light;
color: $es-primary;
}
// Country holidays variant - use calendar icon style
&.country-holidays {
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
&:hover {
background: rgba(139, 92, 246, 0.2);
}
&.clickable {
background: rgba(139, 92, 246, 0.15);
}
}
i {
font-size: 12px !important;
}
}
}

View File

@@ -0,0 +1,644 @@
/**
* List Preview Component
* Popover and modal views for entity preview
*
* Uses shared entity-item base for item styling.
* This file only contains popover/modal container styles.
*/
@use '../variables' as *;
@use '../mixins' as *;
// =============================================================================
// Preview Popover Container
// =============================================================================
.target-preview-popover,
.target-list-preview-popover {
position: absolute;
z-index: 10000;
min-width: 320px;
max-width: 480px;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
box-shadow: $es-shadow-lg;
overflow: hidden;
// Arrow pointing to badge
&::before {
content: '';
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid $es-border-color;
}
&::after {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid $es-white;
}
// Positioned to the right - arrow on left
&.position-right {
&::before,
&::after {
left: 20px;
transform: none;
}
}
// Positioned to the left - arrow on right
&.position-left {
&::before,
&::after {
left: auto;
right: 20px;
transform: none;
}
}
}
// =============================================================================
// Preview Header
// =============================================================================
.preview-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-title {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.preview-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-primary;
}
}
}
// =============================================================================
// Preview Tabs (entity type switcher)
// =============================================================================
.preview-tabs {
display: flex;
flex-wrap: wrap;
gap: 0;
padding: 0;
background: $es-slate-50;
border-bottom: 1px solid $es-border-color;
}
.preview-tab {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
color: $es-text-muted;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
cursor: pointer;
transition: all $es-transition-fast;
white-space: nowrap;
&:hover {
background: $es-slate-100;
color: $es-text-secondary;
}
&.active {
background: $es-white;
border-bottom-color: $es-primary;
color: $es-primary;
}
i {
font-size: 12px;
}
}
// =============================================================================
// Preview Filter
// =============================================================================
.preview-filter {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-white;
border-bottom: 1px solid $es-border-color;
i {
color: $es-text-muted;
font-size: 12px;
}
.preview-filter-input {
all: unset;
flex: 1;
padding: 0.25rem 0;
font-size: $es-font-size-xs;
color: $es-text-primary;
box-sizing: border-box;
&::placeholder {
color: $es-text-muted;
}
}
}
// =============================================================================
// Preview Contents (tabbed content areas)
// =============================================================================
.preview-contents {
max-height: 350px;
overflow: hidden;
}
.preview-content {
display: none;
max-height: 350px;
overflow-y: auto;
@include custom-scrollbar;
&.active {
display: block;
}
}
// =============================================================================
// Preview Items Container
// =============================================================================
.preview-items {
display: flex;
flex-direction: column;
padding: $es-spacing-xs $es-spacing-sm;
}
// =============================================================================
// Preview Item - Uses entity-item patterns
// Maps legacy classes to shared styling
// =============================================================================
.preview-item {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm;
background: $es-white;
border-radius: $es-radius-sm;
transition: background $es-transition-fast;
&:hover {
background: $es-bg-hover;
}
// Clickable items
&[data-id] {
cursor: pointer;
}
}
// Image - matches chip image sizing for consistency
.preview-item-image {
flex-shrink: 0;
width: 32px;
height: 32px;
object-fit: cover;
border-radius: $es-radius-sm;
background: $es-slate-100;
}
// No-image placeholder
.preview-item-no-image {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 32px;
height: 32px;
background: $es-slate-100;
color: $es-text-muted;
border-radius: $es-radius-sm;
font-size: $es-font-size-sm;
}
// Info container
.preview-item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
// Name
.preview-item-name {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
color: $es-text-primary;
@include text-truncate;
}
// Meta/ref (category, email, etc.)
.preview-item-ref,
.preview-item-meta {
font-size: $es-font-size-xs;
color: $es-text-muted;
@include text-truncate;
}
// Price badge
.preview-item-price {
flex-shrink: 0;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
color: $es-primary;
background: $es-primary-light;
border-radius: $es-radius-sm;
}
// =============================================================================
// Preview Footer (load more)
// =============================================================================
.preview-footer {
padding: $es-spacing-sm $es-spacing-md;
background: $es-slate-50;
border-top: 1px solid $es-border-color;
}
.load-more-controls {
display: flex;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
font-size: $es-font-size-xs;
color: $es-text-muted;
.load-more-label {
white-space: nowrap;
}
.load-more-select {
appearance: none;
padding: 0.25rem 1.75rem 0.25rem 0.5rem;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
background: $es-white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23666' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E") no-repeat right 0.5rem center;
background-size: 8px;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-primary;
cursor: pointer;
transition: all $es-transition-fast;
height: auto;
min-height: 0;
line-height: 1.3;
&:hover {
border-color: $es-primary;
background-color: $es-primary-light;
}
&:focus {
outline: none;
border-color: $es-primary;
box-shadow: 0 0 0 2px rgba($es-primary, 0.1);
}
}
.load-more-of {
white-space: nowrap;
}
.remaining-count {
font-weight: $es-font-weight-semibold;
color: $es-text-secondary;
}
.btn-load-more {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-xs;
margin: 0;
border: none;
color: $es-primary;
background: $es-primary-light;
border-radius: $es-radius-sm;
cursor: pointer;
transition: all $es-transition-fast;
font: inherit;
i {
font-size: 14px;
}
&:hover {
background: rgba($es-primary, 0.2);
}
&.loading {
cursor: wait;
i {
animation: spin 0.6s linear infinite;
}
}
}
}
// =============================================================================
// Preview States
// =============================================================================
.preview-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-xl;
text-align: center;
color: $es-text-muted;
i {
font-size: 2rem;
opacity: 0.5;
}
p {
margin: 0;
font-size: $es-font-size-sm;
}
}
.preview-loading {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-xl;
color: $es-text-muted;
i {
font-size: 20px;
color: $es-primary;
animation: spin 0.6s linear infinite;
}
}
// =============================================================================
// 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;
gap: $es-spacing-md;
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 {
flex-shrink: 0;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-muted;
background: $es-slate-200;
padding: 0.125rem 0.5rem;
border-radius: $es-radius-sm;
}
}
.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;
}
}
// =============================================================================
// Schedule Dropdown Preview
// Inline dropdown for schedule details in admin list
// =============================================================================
.mpr-dropdown-preview {
display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 6px;
z-index: 1000;
padding: 0.625rem 0.75rem;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
box-shadow: $es-shadow-lg;
font-size: 12px;
text-align: left;
text-transform: none;
font-weight: normal;
white-space: nowrap; // Allow dropdown to grow as needed
&.is-open {
display: block;
}
}
.mpr-dropdown-preview__item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0;
color: #666;
line-height: 1.4;
&:not(:last-child) {
margin-bottom: 0.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba($es-border-color, 0.5);
}
// Icon styles
> .material-icons,
> i:first-child {
flex-shrink: 0;
width: 16px;
font-size: 14px !important;
color: #999;
text-align: center;
}
}
.mpr-dropdown-preview__muted {
color: #999;
}
// Schedule inline layout
.mpr-dropdown-preview__schedule {
display: inline;
}
.mpr-dropdown-preview__schedule-row {
display: inline;
&:not(:last-child)::after {
content: ',';
margin-right: 0.5rem;
}
}
.mpr-dropdown-preview__day {
color: #666;
}
.mpr-dropdown-preview__hours {
color: $es-primary;
font-weight: 500;
margin-left: 0.25rem;
}
// Badge trigger for preview dropdown/popover
.mpr-badge--preview {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
position: relative;
min-width: 20px;
height: 20px;
padding: 0 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 50rem;
cursor: pointer;
text-transform: none;
.material-icons {
font-size: 12px !important;
line-height: 1;
opacity: 0.8;
}
}
.mpr-badge--preview-primary {
background: $es-primary;
color: $es-white;
}
.mpr-badge--preview-success {
background: #d4edda;
color: #155724;
}
.mpr-badge--preview-warning {
background: #fff3cd;
color: #856404;
}
.mpr-badge--preview-muted {
background: $es-slate-200;
color: $es-text-muted;
}

View File

@@ -0,0 +1,225 @@
/**
* Method Dropdown Component
* Custom select dropdown with icons for method selection
*/
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Method dropdown trigger button
.method-dropdown-trigger {
display: inline-flex;
align-items: center;
gap: 0.5rem;
height: 36px;
padding: 0 $es-spacing-md;
border-radius: $es-radius-md;
background: $es-white;
color: $es-slate-800;
font-size: $es-font-size-sm;
cursor: pointer;
transition: all $es-transition-fast;
min-width: 180px;
max-width: 320px;
border: 1px solid $es-border-color;
&:hover {
background: $es-slate-50;
border-color: $es-gray-300;
}
&:focus,
&:active {
outline: none;
border-color: $es-primary;
box-shadow: 0 0 0 3px rgba($es-primary, 0.1);
}
}
.method-trigger-icon {
font-size: $es-font-size-sm;
color: $es-text-muted;
flex-shrink: 0;
width: 18px;
text-align: center;
}
.method-trigger-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: $es-font-weight-medium;
}
.method-trigger-caret {
font-size: $es-font-size-xs;
color: $es-slate-400;
flex-shrink: 0;
margin-left: auto;
}
// Locked state
.selector-locked .method-dropdown-trigger {
background: $es-slate-100;
color: $es-slate-400;
cursor: not-allowed;
border-color: $es-border-color;
&:hover {
background: $es-slate-100;
border-color: $es-border-color;
}
}
// Method selector wrapper
.method-selector-wrapper {
position: relative;
}
// Hidden select (for form submission)
.method-select-hidden {
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)
// =============================================================================
.method-dropdown-menu {
position: absolute;
z-index: $es-z-dropdown + 1;
min-width: 200px;
max-width: 360px;
max-height: 400px;
overflow-y: auto;
background: $es-white;
border-radius: $es-radius-lg;
padding: 0.375rem 0;
border: 1px solid $es-border-color;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
animation: methodDropdownFadeIn 0.15s ease;
@include custom-scrollbar;
}
@keyframes methodDropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// Method dropdown item
.method-dropdown-item {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.5rem $es-spacing-md;
cursor: pointer;
transition: background-color 0.1s;
position: relative;
&:hover {
background: $es-slate-100;
}
&.selected {
background: rgba($es-primary, 0.08);
}
.method-item-icon {
font-size: $es-font-size-sm;
color: $es-text-muted;
width: 18px;
text-align: center;
flex-shrink: 0;
}
&.selected .method-item-icon {
color: $es-primary;
}
.method-item-label {
flex: 1;
font-size: $es-font-size-sm;
color: $es-slate-700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.selected .method-item-label {
color: $es-cyan-700;
font-weight: $es-font-weight-medium;
}
.method-item-check {
font-size: $es-font-size-xs;
flex-shrink: 0;
margin-left: auto;
color: $es-primary;
}
}
// Method dropdown optgroup
.method-dropdown-optgroup {
margin-top: 0.25rem;
&:first-child {
margin-top: 0;
}
}
.method-optgroup-label {
padding: 0.5rem $es-spacing-md;
font-size: 11px;
font-weight: $es-font-weight-semibold;
color: $es-text-muted;
text-transform: uppercase;
letter-spacing: 0.05em;
background: $es-slate-50;
border-top: 1px solid $es-slate-100;
border-bottom: 1px solid $es-slate-100;
.method-dropdown-optgroup:first-child & {
border-top: 0;
}
}
.method-optgroup-items {
padding: 0.25rem 0;
.method-dropdown-item {
padding-left: $es-spacing-lg;
}
}
// Method info placeholder
.method-info-placeholder {
font-size: $es-font-size-xs;
color: $es-text-muted;
font-style: italic;
}

View File

@@ -0,0 +1,488 @@
/**
* Modal Component
* Preview modals, confirmation dialogs
*/
@use "sass:color";
@use '../variables' as *;
@use '../mixins' as *;
// Modal backdrop
.mpr-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: $es-z-modal;
opacity: 0;
transition: opacity $es-transition-normal;
&.show {
opacity: 1;
}
}
// Modal container
.mpr-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
z-index: $es-z-modal + 1;
width: 90%;
max-width: 600px;
max-height: 90vh;
background: $es-white;
border-radius: $es-radius-xl;
box-shadow: $es-shadow-xl;
opacity: 0;
transition: all $es-transition-normal;
overflow: hidden;
display: flex;
flex-direction: column;
&.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
&.modal-sm {
max-width: 400px;
}
&.modal-lg {
max-width: 800px;
}
&.modal-xl {
max-width: 1000px;
}
&.modal-fullscreen {
width: 95%;
max-width: none;
height: 90vh;
max-height: none;
}
}
// Modal header
.mpr-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-md;
padding: $es-spacing-md $es-spacing-lg;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
flex-shrink: 0;
}
.mpr-modal-title {
font-size: $es-font-size-base;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
margin: 0;
}
.mpr-modal-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: $es-text-muted;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i {
font-size: $es-font-size-lg;
}
}
// Modal body
.mpr-modal-body {
flex: 1;
overflow-y: auto;
padding: $es-spacing-lg;
@include custom-scrollbar;
}
// Modal footer
.mpr-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: $es-spacing-sm;
padding: $es-spacing-md $es-spacing-lg;
background: $es-bg-header;
border-top: 1px solid $es-border-color;
flex-shrink: 0;
}
.mpr-modal-btn {
@include button-reset;
display: inline-flex;
align-items: center;
justify-content: center;
gap: $es-spacing-xs;
padding: $es-spacing-sm $es-spacing-md;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&.btn-secondary {
color: $es-text-secondary;
background: $es-slate-100;
&:hover {
background: $es-slate-200;
}
}
&.btn-primary {
color: $es-white;
background: $es-primary;
&:hover {
background: $es-primary-hover;
}
}
&.btn-danger {
color: $es-white;
background: $es-danger;
&:hover {
background: color.adjust($es-danger, $lightness: -10%);
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Preview popover styles moved to _list-preview.scss
.popover-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
}
.popover-title {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.popover-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
}
.popover-body {
max-height: 300px;
overflow-y: auto;
padding: $es-spacing-sm;
@include custom-scrollbar;
}
.popover-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-top: 1px solid $es-border-color;
border-radius: 0 0 $es-radius-lg $es-radius-lg;
}
.popover-info {
font-size: $es-font-size-xs;
color: $es-text-muted;
}
.popover-load-more {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-primary;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
}
// Popover arrow
.popover-arrow {
position: absolute;
width: 12px;
height: 12px;
background: $es-white;
border: 1px solid $es-border-color;
transform: rotate(45deg);
&.arrow-top {
top: -7px;
left: 50%;
margin-left: -6px;
border-right: none;
border-bottom: none;
}
&.arrow-bottom {
bottom: -7px;
left: 50%;
margin-left: -6px;
border-left: none;
border-top: none;
}
}
// ==========================================================================
// Holiday Preview Modal
// ==========================================================================
#mpr-holiday-preview-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $es-z-modal;
&.show {
display: flex;
align-items: center;
justify-content: center;
}
.mpr-modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.mpr-modal-dialog {
position: relative;
width: 90%;
max-width: 480px;
max-height: 80vh;
background: $es-white;
border-radius: $es-radius-lg;
box-shadow: $es-shadow-xl;
display: flex;
flex-direction: column;
overflow: hidden;
}
.mpr-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-md;
padding: $es-spacing-md $es-spacing-lg;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
flex-shrink: 0;
}
.mpr-modal-title {
display: flex;
align-items: center;
gap: $es-spacing-sm;
font-size: $es-font-size-base;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
margin: 0;
i.material-icons {
font-size: 20px !important;
color: $es-primary;
}
}
.mpr-modal-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: $es-text-muted;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i {
font-size: 18px;
}
}
.mpr-modal-body {
flex: 1;
overflow-y: auto;
padding: $es-spacing-lg;
@include custom-scrollbar;
}
// Loading state
.holiday-preview-loading {
display: flex;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-xl 0;
color: $es-text-muted;
font-size: $es-font-size-sm;
i {
font-size: $es-font-size-lg;
}
}
// Empty state
.holiday-preview-empty {
text-align: center;
padding: $es-spacing-xl 0;
color: $es-text-muted;
i.material-icons {
font-size: 48px !important;
opacity: 0.5;
margin-bottom: $es-spacing-md;
}
p {
margin: 0 0 $es-spacing-xs;
}
.hint {
font-size: $es-font-size-xs;
color: $es-text-muted;
}
}
// Holiday list
.holiday-list {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.holiday-item {
display: flex;
align-items: flex-start;
gap: $es-spacing-md;
padding: $es-spacing-sm $es-spacing-md;
background: $es-slate-50;
border-radius: $es-radius-md;
border-left: 3px solid $es-success;
&.holiday-type-bank {
border-left-color: $es-info;
}
&.holiday-type-observance {
border-left-color: $es-warning;
}
&.holiday-type-regional {
border-left-color: #8b5cf6;
}
}
.holiday-date {
flex-shrink: 0;
min-width: 100px;
.holiday-day {
display: block;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.holiday-weekday {
display: block;
font-size: $es-font-size-xs;
color: $es-text-muted;
}
}
.holiday-info {
flex: 1;
min-width: 0;
}
.holiday-name {
display: block;
font-size: $es-font-size-sm;
color: $es-text-primary;
word-wrap: break-word;
}
.holiday-type-badge {
display: inline-block;
margin-top: $es-spacing-xs;
padding: 0.125rem 0.375rem;
font-size: 10px;
font-weight: $es-font-weight-medium;
text-transform: capitalize;
background: $es-slate-200;
color: $es-text-secondary;
border-radius: $es-radius-sm;
}
.holiday-preview-note {
margin-top: $es-spacing-md;
font-size: $es-font-size-xs;
color: $es-text-muted;
text-align: center;
}
}

View File

@@ -0,0 +1,369 @@
/**
* Schedule Conditions Component
* DateTime picker, weekly timeline, holidays
*/
@use '../variables' as *;
@use '../mixins' as *;
// Schedule conditions wrapper
.schedule-conditions-trait {
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
}
// Schedule header
.schedule-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-md;
padding: 0.875rem $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
border-radius: $es-radius-lg $es-radius-lg 0 0;
cursor: pointer;
user-select: none;
transition: background-color $es-transition-fast;
&:hover {
background: $es-bg-hover;
}
}
.schedule-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
i {
color: $es-text-muted;
}
}
// Schedule body
.schedule-body {
padding: $es-spacing-md;
}
// Schedule section
.schedule-section {
margin-bottom: $es-spacing-lg;
&:last-child {
margin-bottom: 0;
}
}
.schedule-section-title {
display: flex;
align-items: center;
gap: $es-spacing-sm;
margin-bottom: $es-spacing-sm;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
i {
color: $es-text-muted;
}
}
.schedule-section-description {
margin-bottom: $es-spacing-md;
font-size: $es-font-size-xs;
color: $es-text-muted;
}
// DateTime range picker
.datetime-range {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-md;
}
.datetime-field {
flex: 1;
min-width: 200px;
}
.datetime-label {
display: block;
margin-bottom: 0.25rem;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-secondary;
}
.datetime-input {
@include input-base;
}
// Weekly schedule
.weekly-schedule {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.day-row {
display: flex;
align-items: center;
gap: $es-spacing-md;
padding: $es-spacing-sm;
background: $es-slate-50;
border-radius: $es-radius-md;
&.disabled {
opacity: 0.5;
}
}
.day-toggle {
display: flex;
align-items: center;
gap: $es-spacing-sm;
min-width: 100px;
}
.day-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.day-name {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
color: $es-text-primary;
}
// Timeline slider
.timeline-slider {
flex: 1;
position: relative;
height: 24px;
background: $es-slate-200;
border-radius: $es-radius-full;
cursor: pointer;
}
.timeline-fill {
position: absolute;
top: 0;
height: 100%;
background: $es-primary;
border-radius: $es-radius-full;
transition: all $es-transition-fast;
}
.timeline-handle {
position: absolute;
top: 50%;
width: 16px;
height: 16px;
background: $es-white;
border: 2px solid $es-primary;
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: grab;
box-shadow: $es-shadow-sm;
transition: box-shadow $es-transition-fast;
&:hover {
box-shadow: $es-shadow-md;
}
&:active {
cursor: grabbing;
}
&.handle-start {
z-index: 2;
}
&.handle-end {
z-index: 1;
}
}
// Time display
.day-times {
display: flex;
align-items: center;
gap: $es-spacing-xs;
min-width: 120px;
font-size: $es-font-size-xs;
font-family: monospace;
color: $es-text-secondary;
}
.time-separator {
color: $es-text-muted;
}
// Holiday exclusions
.holiday-section {
padding: $es-spacing-md;
background: $es-slate-50;
border-radius: $es-radius-md;
}
.holiday-toggle {
display: flex;
align-items: center;
gap: $es-spacing-sm;
margin-bottom: $es-spacing-md;
}
.holiday-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.holiday-label {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
color: $es-text-primary;
}
.holiday-countries {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
}
.holiday-country-chip {
@include chip;
cursor: pointer;
&.selected {
background: $es-primary-light;
color: $es-primary;
}
}
// Server time display
.server-time {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-info-light;
border-radius: $es-radius-md;
font-size: $es-font-size-xs;
color: $es-info;
i {
font-size: $es-font-size-sm;
}
.time-value {
font-family: monospace;
font-weight: $es-font-weight-semibold;
}
}
// Schedule summary
.schedule-summary {
display: flex;
flex-direction: column;
gap: $es-spacing-xs;
padding: $es-spacing-md;
background: $es-slate-50;
border-radius: $es-radius-md;
font-size: $es-font-size-sm;
color: $es-text-secondary;
.summary-item {
display: flex;
align-items: center;
gap: $es-spacing-sm;
i {
color: $es-success;
font-size: $es-font-size-sm;
}
&.inactive i {
color: $es-text-muted;
}
}
}
// Collapsed state
.schedule-conditions-trait.collapsed {
.schedule-body {
display: none;
}
.schedule-header {
border-radius: $es-radius-lg;
}
}
// Schedule toggle row (form-content layout)
.schedule-toggle-row {
display: flex;
align-items: center;
background: $es-slate-100;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
.schedule-toggle-switch {
padding: $es-spacing-sm $es-spacing-md;
}
.schedule-toggle-actions {
padding: $es-spacing-sm $es-spacing-md;
border-left: 1px solid $es-border-color;
cursor: pointer;
transition: background-color $es-transition-fast;
&:hover {
background: $es-slate-200;
}
.material-icons {
color: $es-slate-400;
font-size: 20px !important;
}
}
}
// Schedule summary badges (read-only indicators in header)
.schedule-summary-badges {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
padding: 0 $es-spacing-sm;
}
.schedule-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: $es-slate-200;
color: $es-slate-600;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-full;
white-space: nowrap;
.material-icons {
font-size: 14px !important;
opacity: 0.7;
}
}
// Section hint after embedded entity selector - add margin
.schedule-holidays .section-hint {
margin-top: $es-spacing-md;
}

View File

@@ -0,0 +1,142 @@
/**
* Tips Box Component
* Pro tips and help information display
*/
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Tips box container
.target-tips-box {
margin: $es-spacing-lg $es-spacing-md $es-spacing-md;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
overflow: hidden;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
// Tips header (clickable to expand/collapse)
.tips-header {
display: flex;
align-items: center;
gap: 0.625rem;
padding: $es-spacing-md $es-spacing-lg;
cursor: pointer;
user-select: none;
transition: background-color $es-transition-fast;
&:hover {
background: rgba(0, 0, 0, 0.02);
}
// Lightbulb icon
> i:first-child {
font-size: 1rem;
color: $es-warning;
}
// Title text
> span {
flex: 1;
font-size: 13px;
font-weight: $es-font-weight-semibold;
color: $es-slate-600;
}
}
// Toggle chevron icon
.tips-toggle {
font-size: $es-font-size-xs;
color: $es-slate-400;
transition: transform 0.2s;
}
// Expanded state
.target-tips-box.expanded {
.tips-toggle {
transform: rotate(180deg);
}
.tips-content {
display: block;
}
}
// Tips content (hidden by default)
.tips-content {
display: none;
padding: 0 $es-spacing-lg $es-spacing-lg;
}
// Tips grid layout
.tips-grid {
display: grid;
gap: $es-spacing-md;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
// Individual tip item
.tip-item {
display: flex;
gap: $es-spacing-md;
padding: $es-spacing-md;
background: $es-white;
border-radius: $es-radius-md;
border: 1px solid $es-border-color;
}
// Tip icon
.tip-icon {
flex-shrink: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: $es-primary-light;
border-radius: $es-radius-md;
color: $es-primary;
font-size: $es-font-size-sm;
}
// Tip text content
.tip-text {
flex: 1;
min-width: 0;
strong {
display: block;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
color: $es-slate-700;
margin-bottom: 0.25rem;
}
p {
font-size: 11px;
color: $es-text-muted;
line-height: 1.625;
margin: 0;
}
}
// Tips footer
.tips-footer {
margin-top: $es-spacing-md;
padding: 0.625rem $es-spacing-md;
background: $es-white;
border-radius: $es-radius-md;
border: 1px dashed $es-gray-300;
font-size: 11px;
color: $es-text-muted;
line-height: 1.625;
i {
color: $es-primary;
margin-right: 0.25rem;
}
}
}

View File

@@ -0,0 +1,107 @@
/**
* Tooltip Component
* Info tooltips for method help
*/
@use '../variables' as *;
// =============================================================================
// Info Wrapper (tooltip trigger)
// =============================================================================
.mpr-info-wrapper {
display: inline-flex;
align-items: center;
position: relative;
cursor: help;
vertical-align: middle;
margin-left: 0.25rem;
.material-icons {
font-size: 16px !important;
color: $es-text-muted;
transition: color 0.15s ease;
}
&:hover .material-icons {
color: $es-primary;
}
}
// =============================================================================
// Fixed Tooltip (appended to body on hover)
// =============================================================================
.mpr-tooltip-fixed {
position: fixed;
background: $es-white;
color: $es-slate-800;
padding: $es-spacing-md $es-spacing-lg;
border-radius: $es-radius-md;
font-size: 13px;
line-height: 1.5;
white-space: normal;
z-index: 10500;
max-width: 320px;
min-width: 180px;
text-align: left;
box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 1px 0px,
rgba(64, 68, 82, 0.16) 0px 0px 0px 1px,
rgba(64, 68, 82, 0.08) 0px 2px 5px 0px;
pointer-events: none;
// Pinned tooltip allows interaction
&.pinned {
pointer-events: auto;
padding-right: $es-spacing-xl + 1rem;
}
strong {
display: block;
margin-bottom: 0.375rem;
font-weight: $es-font-weight-semibold;
color: $es-primary;
}
p {
margin: 0;
color: $es-text-secondary;
}
ul {
margin: 0.5rem 0 0;
padding-left: 1.25rem;
li {
margin: 0.25rem 0;
color: $es-text-secondary;
}
}
}
// Close button for pinned tooltips
.mpr-tooltip-close {
position: absolute;
top: 0.375rem;
right: 0.375rem;
padding: 0.125rem;
border: none;
background: transparent;
cursor: pointer;
border-radius: $es-radius-sm;
line-height: 1;
transition: background-color 0.15s ease;
.material-icons {
font-size: 16px !important;
color: $es-text-muted;
}
&:hover {
background: $es-slate-100;
.material-icons {
color: $es-slate-700;
}
}
}

View File

@@ -0,0 +1,343 @@
/**
* Category Tree Component
* Hierarchical tree view for category selection inside dropdown
*/
@use "sass:color";
@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: color.adjust($es-primary, $lightness: -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;
}
}

View File

@@ -0,0 +1,87 @@
/**
* Validation Toast Component
* Error notifications for selection conflicts
*/
@use '../variables' as *;
@use '../mixins' as *;
// Validation error toast
.es-validation-toast {
display: flex;
align-items: flex-start;
gap: $es-spacing-sm;
padding: $es-spacing-md;
background: $es-white;
border: 1px solid $es-danger;
border-left: 4px solid $es-danger;
border-radius: $es-radius-md;
box-shadow: $es-shadow-lg;
max-width: 400px;
animation: es-toast-slide-in 0.2s ease-out;
.es-toast-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-danger;
flex-shrink: 0;
i {
font-size: 18px;
}
}
.es-toast-content {
flex: 1;
min-width: 0;
}
.es-toast-title {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-danger;
margin-bottom: 2px;
}
.es-toast-message {
font-size: $es-font-size-xs;
color: $es-text-secondary;
line-height: 1.4;
}
.es-toast-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: $es-text-muted;
border-radius: $es-radius-sm;
flex-shrink: 0;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-100;
color: $es-text-primary;
}
i {
font-size: 12px;
}
}
}
@keyframes es-toast-slide-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,281 @@
/**
* Value Picker Component
* Search boxes, input types, range inputs
*/
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Value picker container
.value-picker {
padding: $es-spacing-sm 0;
&[style*="display: none"],
&[style*="display:none"] {
padding: 0;
}
}
.include-picker,
.exclude-picker {
// Section-specific picker styles
}
// Entity search box
.entity-search-box {
position: relative;
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-xs;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:focus-within {
border-color: $es-primary;
box-shadow: 0 0 0 2px rgba($es-primary, 0.1);
}
}
// 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
input.entity-search-input,
input.entity-search-input[type="text"] {
@include input-reset;
flex: 1;
min-width: 0;
width: auto !important;
max-width: none !important;
padding: 0.375rem;
font-size: $es-font-size-sm;
color: $es-text-primary;
border: none !important;
background: transparent !important;
box-shadow: none !important;
&::placeholder {
color: $es-text-muted;
}
&:focus {
border: none !important;
box-shadow: none !important;
outline: none;
}
}
.search-loading {
display: flex;
align-items: center;
justify-content: center;
color: $es-primary;
i {
animation: spin 0.6s linear infinite;
}
}
// 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 {
display: flex;
align-items: center;
gap: $es-spacing-xs;
}
.range-min-input,
.range-max-input {
@include input-base;
width: 100px;
padding: $es-spacing-sm;
text-align: center;
font-size: $es-font-size-sm;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
-moz-appearance: textfield;
}
.range-separator {
color: $es-text-muted;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
}
.btn-add-range {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: $es-white;
background: $es-primary;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-hover;
}
i {
font-size: 12px;
}
}
// Multi-range container
.multi-range-container {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
// Date range box
.date-range-box {
display: flex;
align-items: center;
gap: $es-spacing-xs;
}
.date-from-input,
.date-to-input {
@include input-base;
width: 140px;
padding: $es-spacing-sm;
font-size: $es-font-size-sm;
}
// Multi-select tiles
.multi-select-tiles {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
}
.tile-option {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
color: $es-text-muted;
background: transparent;
border: 1px dashed $es-border-color;
border-radius: 100px; // Pill shape
font-size: $es-font-size-xs;
font-weight: $es-font-weight-normal;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
color: $es-text-secondary;
border-color: $es-slate-400;
border-style: solid;
}
&.selected {
color: $es-primary;
background: $es-primary-light;
border: 1px solid $es-primary;
font-weight: $es-font-weight-medium;
}
i {
font-size: 11px;
opacity: 0.6;
}
&.selected i {
opacity: 1;
}
}
.tile-label {
white-space: nowrap;
}
// Select input box
.select-input-box {
display: inline-block;
}
.select-value-input {
@include input-base;
padding: $es-spacing-sm $es-spacing-md;
font-size: $es-font-size-sm;
min-width: 150px;
}
// Boolean input box
.boolean-input-box {
display: inline-flex;
align-items: center;
padding: $es-spacing-sm $es-spacing-md;
background: $es-success-light;
color: $es-success-dark;
border-radius: $es-radius-md;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
}
.boolean-label {
display: flex;
align-items: center;
gap: 0.25rem;
&::before {
content: '\2713';
font-weight: bold;
}
}
// Condition match count badge
.condition-match-count {
@include count-badge($es-primary);
margin-left: $es-spacing-sm;
}
}

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1,161 @@
/**
* Form Integration Styles
* Handles PrestaShop admin form layout overrides
*/
@use '../variables' as *;
// Base border reset for all entity-selector elements
.target-conditions-trait,
.target-conditions-trait *,
.entity-selector-trait,
.entity-selector-trait *,
.method-dropdown-menu,
.method-dropdown-menu *,
.target-preview-popover,
.target-preview-popover * {
border-style: solid;
border-width: 0;
border-color: $es-border-color;
}
// Full-width form group override using :has()
// Excludes .layout-form-group which uses standard PrestaShop form layout
.form-group:has(.entity-selector-trait:not(.layout-form-group)),
.form-group:has(.target-conditions-trait:not(.layout-form-group)),
.form-group:has(.condition-trait:not(.layout-form-group)) {
display: block;
> .control-label {
display: none;
}
> .col-lg-8 {
width: 100%;
max-width: 100%;
padding-left: $es-spacing-md;
padding-right: $es-spacing-md;
flex: 0 0 100% !important;
}
}
// Fallback class for browsers without :has() support
.form-group.condition-trait-fullwidth {
display: block;
> .control-label {
display: none;
}
> .col-lg-8 {
width: 100%;
max-width: 100%;
padding-left: $es-spacing-md;
padding-right: $es-spacing-md;
flex: 0 0 100% !important;
}
}
// SAFEGUARD: Force label visibility for form-group layout widgets
// This overrides any conflicting rules (including fallback class rules)
// when the widget has layout-form-group class indicating standard form integration
.form-group:has(.layout-form-group) > .control-label {
display: flex !important;
}
// Dropdown overflow fix
// When dropdown is open, parent containers must allow overflow
.panel:has(.target-search-dropdown.show),
.card:has(.target-search-dropdown.show),
.form-wrapper:has(.target-search-dropdown.show),
.panel-body:has(.target-search-dropdown.show),
.card-body:has(.target-search-dropdown.show),
.form-group:has(.target-search-dropdown.show),
.col-lg-8:has(.target-search-dropdown.show),
.col-lg-12:has(.target-search-dropdown.show) {
overflow: visible !important;
}
// Target conditions wrapper hierarchy overflow fix
.target-conditions-trait:has(.target-search-dropdown.show),
.entity-selector-trait:has(.target-search-dropdown.show),
.condition-trait-body:has(.target-search-dropdown.show),
.target-block-content:has(.target-search-dropdown.show),
.target-block-groups:has(.target-search-dropdown.show),
.target-group:has(.target-search-dropdown.show),
.target-group-body:has(.target-search-dropdown.show),
.target-search-wrapper:has(.target-search-dropdown.show) {
overflow: visible !important;
}
// =============================================================================
// Embedded Layout
// =============================================================================
// Use .layout-embedded for entity selectors nested inside other components
// Removes outer wrapper styling to avoid redundant borders/backgrounds
.target-conditions-trait.layout-embedded,
.entity-selector-trait.layout-embedded {
background: transparent;
border: none;
border-radius: 0;
// Remove padding from groups container when embedded
.groups-container {
padding: 0;
}
// Remove block body padding
.block-body {
padding: 0;
}
// Remove block footer border when embedded
.block-footer {
border-top: none;
padding: $es-spacing-sm 0 0;
}
// Simplify selection group when embedded - single thin border only
.selection-group {
background: $es-white;
border: 1px solid $es-slate-200;
border-radius: $es-radius-md;
// Lighter group header in embedded mode
.group-header {
background: $es-slate-50;
border-bottom-color: $es-slate-200;
padding: $es-spacing-xs $es-spacing-sm;
border-radius: $es-radius-md $es-radius-md 0 0;
}
// Reduce group body padding (slightly more than $es-spacing-sm for readability)
.group-body {
padding: 0.75rem;
}
// Reduce group-include section padding
.group-include {
padding: $es-spacing-xs;
margin-bottom: $es-spacing-sm;
}
// Smaller modifiers section
.group-modifiers {
padding: $es-spacing-xs $es-spacing-sm;
margin: $es-spacing-sm (-$es-spacing-sm) (-$es-spacing-sm);
}
}
// Empty state - smaller padding
.groups-empty-state {
padding: $es-spacing-md;
}
// Smaller add group button
.btn-add-group {
padding: 0.375rem 0.625rem;
font-size: $es-font-size-xs;
}
}

View File

@@ -0,0 +1,63 @@
/**
* Responsive Styles
* Media query adjustments for different screen sizes
*/
@use '../variables' as *;
// Tablet and below
@media (max-width: 991px) {
.target-conditions-trait,
.entity-selector-trait {
.condition-trait-header {
flex-direction: column;
align-items: flex-start;
gap: $es-spacing-sm;
}
.trait-header-right {
width: 100%;
justify-content: flex-end;
}
.target-block-tabs {
flex-wrap: wrap;
}
}
}
// Mobile
@media (max-width: 767px) {
.target-conditions-trait,
.entity-selector-trait {
.target-block-tab {
padding: $es-spacing-sm;
font-size: $es-font-size-xs;
}
.target-group-header {
flex-direction: column;
align-items: flex-start;
}
.target-search-dropdown {
width: 100% !important;
left: 0 !important;
right: 0 !important;
}
.dropdown-results-grid {
grid-template-columns: 1fr !important;
}
}
}
// High-resolution displays
@media (min-width: 1600px) {
.target-conditions-trait,
.entity-selector-trait {
.dropdown-results-grid.view-grid-3 {
grid-template-columns: repeat(4, 1fr);
}
}
}

View File

@@ -0,0 +1 @@
<?php header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Location: ../"); exit;

View File

@@ -0,0 +1,33 @@
/**
* Entity Selector Styles
* @package prestashop-entity-selector
* @version 2.0.0
*
* Compiles to: assets/css/admin/entity-selector.css
*/
// Foundation
@use 'variables' as *;
@use 'mixins' as *;
// Layouts
@use 'layouts/form-integration';
@use 'layouts/responsive';
// Components
@use 'components/entity-selector';
@use 'components/entity-item'; // Shared base for chips and list items
@use 'components/dropdown';
@use 'components/chips';
@use 'components/groups';
@use 'components/value-picker';
@use 'components/modal';
@use 'components/list-preview';
@use 'components/schedule';
@use 'components/tips';
@use 'components/condition-trait';
@use 'components/combinations';
@use 'components/method-dropdown';
@use 'components/tooltip';
@use 'components/tree';
@use 'components/validation';

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,210 @@ class EntitySelectorRenderer
$this->translator = $translator;
}
// ---------------------------------------------------------------
// Icon framework abstraction (Material Icons vs FontAwesome 4)
// ---------------------------------------------------------------
/**
* Material Icons → FontAwesome 4 class mapping
*/
private static $fa4Map = [
'account_tree' => 'icon-sitemap',
'add' => 'icon-plus',
'add_box' => 'icon-plus-square',
'arrow_downward' => 'icon-sort-desc',
'arrow_drop_down' => 'icon-caret-down',
'arrow_right' => 'icon-chevron-right',
'arrow_upward' => 'icon-sort-asc',
'block' => 'icon-ban',
'brush' => 'icon-paint-brush',
'business' => 'icon-building',
'check' => 'icon-check',
'check_box' => 'icon-check-square',
'check_box_outline_blank' => 'icon-square-o',
'check_circle' => 'icon-check-circle',
'close' => 'icon-times',
'delete' => 'icon-trash',
'description' => 'icon-file-text',
'error' => 'icon-exclamation-circle',
'event' => 'icon-calendar',
'event_busy' => 'icon-calendar-times-o',
'expand_less' => 'icon-chevron-up',
'expand_more' => 'icon-chevron-down',
'filter_list' => 'icon-filter',
'flag' => 'icon-flag',
'folder' => 'icon-folder',
'folder_open' => 'icon-folder-open',
'indeterminate_check_box' => 'icon-minus-square',
'info' => 'icon-info-circle',
'inventory_2' => 'icon-archive',
'label' => 'icon-tag',
'language' => 'icon-globe',
'lightbulb' => 'icon-lightbulb-o',
'list' => 'icon-list',
'list_alt' => 'icon-list-alt',
'local_shipping' => 'icon-truck',
'lock' => 'icon-lock',
'my_location' => 'icon-crosshairs',
'open_in_full' => 'icon-expand',
'payments' => 'icon-credit-card',
'progress_activity' => 'icon-circle-o-notch',
'schedule' => 'icon-clock-o',
'search' => 'icon-search',
'shopping_cart' => 'icon-shopping-cart',
'sort' => 'icon-sort',
'sort_by_alpha' => 'icon-sort-alpha-asc',
'star' => 'icon-star',
'sync' => 'icon-refresh',
'tune' => 'icon-sliders',
'visibility' => 'icon-eye',
'warning' => 'icon-warning',
'widgets' => 'icon-th-large',
];
/**
* FontAwesome 4 class → Material Icons reverse mapping.
* Built once from $fa4Map on first use.
* @var array|null
*/
private static $reverseFa4Map = null;
/**
* Extra FA4→Material mappings for icon names used in block configs
* that don't appear in the standard fa4Map (e.g. icon-cube, icon-folder-o).
*/
private static $extraReverseMappings = [
'icon-cube' => 'inventory',
'icon-folder-o' => 'folder',
'icon-file-text-o' => 'description',
'icon-briefcase' => 'work',
'icon-user' => 'person',
'icon-users' => 'group',
'icon-money' => 'payments',
'icon-tasks' => 'checklist',
'icon-calculator' => 'calculate',
'icon-asterisk' => 'star',
'icon-bar-chart' => 'bar_chart',
'icon-cogs' => 'settings',
'icon-cog' => 'settings',
'icon-tags' => 'label',
'icon-list-ul' => 'list',
'icon-th' => 'grid_view',
'icon-certificate' => 'verified',
'icon-power-off' => 'power_settings_new',
'icon-circle-o' => 'radio_button_unchecked',
];
/**
* Get the reverse FA4→Material mapping (built lazily from $fa4Map + extras).
*
* @return array
*/
private static function getReverseFa4Map()
{
if (self::$reverseFa4Map === null) {
self::$reverseFa4Map = array_flip(self::$fa4Map);
// Merge extras (extras take priority for icons not in the flipped map)
foreach (self::$extraReverseMappings as $fa4Class => $materialName) {
if (!isset(self::$reverseFa4Map[$fa4Class])) {
self::$reverseFa4Map[$fa4Class] = $materialName;
}
}
}
return self::$reverseFa4Map;
}
/**
* Normalize an icon name to the canonical format for the current mode.
* Handles both Material Icons names and FA4 class names as input.
*
* @param string $name Icon name (Material or FA4 format)
* @return array ['name' => string, 'extra' => string] normalized name + any extra classes
*/
protected function normalizeIconName($name)
{
$extra = '';
// If name starts with 'icon-', it's an FA4 class name
if (strpos($name, 'icon-') === 0) {
// Extract extra CSS classes (e.g. "icon-power-off text-success" → "icon-power-off" + "text-success")
$parts = explode(' ', $name, 2);
$fa4Class = $parts[0];
if (isset($parts[1])) {
$extra = $parts[1];
}
if ($this->getIconMode() === 'material') {
// Reverse map FA4→Material
$reverseMap = self::getReverseFa4Map();
$materialName = $reverseMap[$fa4Class] ?? null;
if ($materialName) {
return ['name' => $materialName, 'extra' => $extra];
}
// Last resort: strip 'icon-' prefix and convert hyphens to underscores
$fallback = str_replace('-', '_', substr($fa4Class, 5));
return ['name' => $fallback, 'extra' => $extra];
}
// Already FA4 and mode is FA4 — use as-is
return ['name' => $fa4Class, 'extra' => $extra, 'raw_fa4' => true];
}
// Material Icons name — use as-is for material mode, map for FA4 mode
return ['name' => $name, 'extra' => $extra];
}
/**
* Detect icon mode based on PrestaShop version.
* PS 8+ / 9+ → material, PS 1.6 / 1.7 → fa4.
*
* @return string 'material' or 'fa4'
*/
protected function getIconMode()
{
return version_compare(_PS_VERSION_, '8.0.0', '>=') ? 'material' : 'fa4';
}
/**
* Render an icon element that works on both legacy and modern PS.
* Accepts both Material Icons names and FA4 class names as input.
*
* @param string $name Icon name (Material or FA4 format, e.g. 'shopping_cart' or 'icon-cube')
* @param string $extraClass Additional CSS class(es)
* @return string HTML
*/
protected function renderIcon($name, $extraClass = '')
{
$normalized = $this->normalizeIconName($name);
$iconName = $normalized['name'];
// Merge extra classes from normalization (e.g. "text-success" from "icon-power-off text-success")
if (!empty($normalized['extra'])) {
$extraClass = $extraClass ? $extraClass . ' ' . $normalized['extra'] : $normalized['extra'];
}
if ($this->getIconMode() === 'material') {
$cls = 'material-icons es-icon';
if ($extraClass) {
$cls .= ' ' . $extraClass;
}
return '<i class="' . $cls . '">' . htmlspecialchars($iconName, ENT_QUOTES, 'UTF-8') . '</i>';
}
// FA4 mode
if (!empty($normalized['raw_fa4'])) {
// Input was already an FA4 class name — use directly
$cls = $iconName . ' es-icon';
} else {
// Input was a Material name — map to FA4
$mapped = self::$fa4Map[$iconName] ?? 'icon-circle';
$cls = $mapped . ' es-icon';
}
if ($extraClass) {
$cls .= ' ' . $extraClass;
}
return '<i class="' . $cls . '"></i>';
}
/**
* Set block definitions
*
@@ -121,7 +325,6 @@ class EntitySelectorRenderer
'show_cms' => true,
'show_cms_categories' => true,
'combination_mode' => 'products',
'product_selection_level' => 'product',
'mode' => 'multi',
'blocks' => [],
'customBlocks' => [],
@@ -140,9 +343,6 @@ class EntitySelectorRenderer
if (is_string($savedData)) {
$savedData = json_decode($savedData, true) ?: [];
}
if (!is_array($savedData)) {
$savedData = [];
}
// Determine which block is active
$enabledBlocks = [];
@@ -166,7 +366,7 @@ class EntitySelectorRenderer
'label' => $blockDef['label'] ?? $blockType,
'entity_label' => $blockDef['label'] ?? $blockType,
'entity_label_plural' => $blockDef['label'] ?? $blockType,
'icon' => $blockDef['icon'] ?? 'icon-cog',
'icon' => $blockDef['icon'] ?? 'settings',
'search_entity' => $blockType,
'selection_methods' => [],
], $blockDef);
@@ -217,6 +417,7 @@ class EntitySelectorRenderer
$html = '<div class="condition-trait target-conditions-trait' . $collapsedClass . $singleModeClass . $requiredClass . $layoutClass . '"';
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
if (!empty($config['required'])) {
$html .= ' data-required="1"';
$requiredMsg = !empty($config['required_message'])
@@ -246,6 +447,7 @@ class EntitySelectorRenderer
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
$html .= '</div>'; // End condition-trait-body
$html .= '</div>'; // End target-conditions-trait
return $html;
@@ -284,6 +486,7 @@ class EntitySelectorRenderer
$html .= '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . '"';
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
if (!empty($config['required'])) {
$html .= ' data-required="1"';
$requiredMsg = !empty($config['required_message'])
@@ -304,7 +507,7 @@ class EntitySelectorRenderer
if ($globalMode !== 'single') {
$html .= '<div class="entity-selector-actions">';
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
$html .= '<i class="material-icons" style="font-size:18px;">unfold_more</i>';
$html .= $this->renderIcon('open_in_full');
$html .= '</button>';
$html .= '</div>';
}
@@ -320,6 +523,7 @@ class EntitySelectorRenderer
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
$html .= '</div>'; // End condition-trait-body
$html .= '</div>'; // End target-conditions-trait
// Subtitle as help text
@@ -358,6 +562,7 @@ class EntitySelectorRenderer
$html = '<div class="condition-trait target-conditions-trait layout-form-group' . $singleModeClass . $requiredClass . $collapsedClass . '"';
$html .= ' data-entity-selector-id="' . $this->escapeAttr($config['id']) . '"';
$html .= ' data-mode="' . $this->escapeAttr($globalMode) . '"';
$html .= ' data-icon-mode="' . $this->getIconMode() . '"';
if (!empty($config['required'])) {
$html .= ' data-required="1"';
$requiredMsg = !empty($config['required_message'])
@@ -373,7 +578,7 @@ class EntitySelectorRenderer
// Actions: expand/collapse toggle (entire area is clickable)
$html .= '<div class="entity-selector-actions btn-toggle-blocks" title="' . $this->trans('Show/hide details') . '">';
$html .= '<i class="material-icons">' . ($collapsed ? 'expand_more' : 'expand_less') . '</i>';
$html .= $this->renderIcon($collapsed ? 'expand_more' : 'expand_less');
$html .= '</div>';
$html .= '</div>'; // End tabs-row
@@ -392,6 +597,7 @@ class EntitySelectorRenderer
// Hidden input (outside collapsed area)
$html .= '<input type="hidden" name="' . $this->escapeAttr($config['name']) . '" value="' . $this->escapeAttr(json_encode($savedData)) . '">';
$html .= '</div>'; // End target-conditions-trait
return $html;
@@ -409,12 +615,12 @@ class EntitySelectorRenderer
{
$html = '<div class="condition-trait-header">';
$html .= '<div class="trait-header-left">';
$html .= '<i class="icon-crosshairs trait-icon"></i>';
$html .= $this->renderIcon('my_location', 'trait-icon');
$html .= '<div class="trait-title-group">';
$html .= '<span class="trait-title">' . $this->escapeAttr($config['title']) . '</span>';
$html .= '<span class="trait-subtitle">' . $this->escapeAttr($config['subtitle']) . '</span>';
$html .= '</div>';
$html .= '<span class="trait-total-count" style="display: none;" title="' . $this->trans('Total items targeted') . '"><i class="icon-eye"></i> <span class="count-value"></span></span>';
$html .= '<span class="trait-total-count" style="display: none;" title="' . $this->trans('Total items targeted') . '">' . $this->renderIcon('visibility') . ' <span class="count-value"></span></span>';
$html .= '</div>';
$html .= '<div class="trait-header-right">';
@@ -429,7 +635,7 @@ class EntitySelectorRenderer
if ($globalMode !== 'single') {
$html .= '<div class="trait-header-actions">';
$html .= '<button type="button" class="btn-toggle-groups" data-state="collapsed" title="' . $this->trans('Expand all groups') . '">';
$html .= '<i class="icon-expand"></i>';
$html .= $this->renderIcon('open_in_full');
$html .= '</button>';
$html .= '</div>';
}
@@ -513,11 +719,11 @@ class EntitySelectorRenderer
$blockMode = $blockSettings[$blockType]['mode'] ?? 'multi';
$html .= '<button type="button" class="target-block-tab' . $activeClass . $hasDataClass . '" data-block-type="' . $this->escapeAttr($blockType) . '" data-block-mode="' . $this->escapeAttr($blockMode) . '">';
$html .= '<i class="' . $this->escapeAttr($blockDef['icon']) . '"></i>';
$html .= $this->renderIcon($blockDef['icon']);
$html .= '<span class="tab-label">' . $this->escapeAttr($blockDef['label']) . '</span>';
if ($hasData) {
// Show loading spinner that will be replaced with actual count
$html .= '<span class="tab-badge loading"><i class="icon-spinner icon-spin"></i></span>';
$html .= '<span class="tab-badge loading">' . $this->renderIcon('sync', 'es-spin') . '</span>';
}
$html .= '</button>';
}
@@ -562,16 +768,16 @@ class EntitySelectorRenderer
{
$html = '<div class="target-tips-box">';
$html .= '<div class="tips-header">';
$html .= '<i class="icon-lightbulb-o"></i>';
$html .= $this->renderIcon('lightbulb');
$html .= '<span>' . $this->trans('Pro Tips: Combine include & exclude for powerful targeting') . '</span>';
$html .= '<i class="icon-chevron-down tips-toggle"></i>';
$html .= $this->renderIcon('expand_more', 'tips-toggle');
$html .= '</div>';
$html .= '<div class="tips-content">';
$html .= '<div class="tips-grid">';
// Example 1
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon"><i class="icon-check-circle"></i></div>';
$html .= '<div class="tip-icon">' . $this->renderIcon('check_circle') . '</div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Target entire catalog with exceptions') . '</strong>';
$html .= '<p>' . $this->trans('Select "All products", then exclude specific categories like "Sale" or "Clearance" where you don\'t want the rule to apply.') . '</p>';
@@ -580,7 +786,7 @@ class EntitySelectorRenderer
// Example 2
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon"><i class="icon-filter"></i></div>';
$html .= '<div class="tip-icon">' . $this->renderIcon('filter_list') . '</div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Combine features for precise filtering') . '</strong>';
$html .= '<p>' . $this->trans('Target all "Cotton" products, then exclude those with "Black" color feature. Perfect for material-specific promotions.') . '</p>';
@@ -589,7 +795,7 @@ class EntitySelectorRenderer
// Example 3
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon"><i class="icon-sitemap"></i></div>';
$html .= '<div class="tip-icon">' . $this->renderIcon('account_tree') . '</div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Category-based targeting') . '</strong>';
$html .= '<p>' . $this->trans('Include entire "Men\'s Clothing" category, exclude "Accessories" subcategory. Hierarchy is respected automatically.') . '</p>';
@@ -598,7 +804,7 @@ class EntitySelectorRenderer
// Example 4
$html .= '<div class="tip-item">';
$html .= '<div class="tip-icon"><i class="icon-building"></i></div>';
$html .= '<div class="tip-icon">' . $this->renderIcon('business') . '</div>';
$html .= '<div class="tip-text">';
$html .= '<strong>' . $this->trans('Brand exclusions') . '</strong>';
$html .= '<p>' . $this->trans('Target all products from "Nike" manufacturer, but exclude items already on sale (by price range or specific products).') . '</p>';
@@ -607,7 +813,7 @@ class EntitySelectorRenderer
$html .= '</div>'; // End tips-grid
$html .= '<div class="tips-footer">';
$html .= '<i class="icon-info-circle"></i> ';
$html .= $this->renderIcon('info') . ' ';
$html .= $this->trans('Multiple groups work as OR logic. Items matching ANY group are included (unless explicitly excluded in that group).');
$html .= '</div>';
$html .= '</div>'; // End tips-content
@@ -707,10 +913,10 @@ class EntitySelectorRenderer
$html .= '<div class="block-footer">';
$html .= '<button type="button" class="btn-add-group">';
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add selection group');
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add selection group');
$html .= '</button>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($groupsTooltip) . '" data-toggle="none">';
$html .= '<i class="material-icons">info</i>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($groupsTooltip) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
$html .= '</div>';
}
@@ -768,11 +974,11 @@ class EntitySelectorRenderer
$html = '<div class="result-modifiers-section">';
$html .= '<div class="result-modifiers-header">';
$html .= '<i class="icon-sliders"></i> ';
$html .= $this->renderIcon('tune') . ' ';
$html .= '<span class="result-modifiers-title">' . $this->trans('Result modifiers') . '</span>';
$html .= '<span class="result-modifiers-hint">' . $this->trans('(optional)') . '</span>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($modifiersTooltip) . '" data-toggle="none">';
$html .= '<i class="material-icons">info</i>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($modifiersTooltip) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
$html .= '</div>';
@@ -911,17 +1117,17 @@ class EntitySelectorRenderer
// Group header
if ($mode === 'single') {
$html .= '<div class="group-header group-header-single">';
$html .= '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
$html .= '<span class="group-count-badge" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '</div>';
} else {
$html .= '<div class="group-header">';
$html .= '<span class="group-collapse-toggle"><i class="icon-chevron-up"></i></span>';
$html .= '<span class="group-collapse-toggle">' . $this->renderIcon('expand_less') . '</span>';
$html .= '<span class="group-name-wrapper">';
$html .= '<input type="text" class="group-name-input" value="' . $groupName . '" placeholder="' . $defaultGroupName . '" title="' . $this->trans('Click to name this group') . '">';
$html .= '<span class="group-count-badge" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
$html .= '<span class="group-count-badge" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '</span>';
$html .= '<button type="button" class="btn-remove-group" title="' . $this->trans('Remove group') . '">';
$html .= '<i class="icon-trash"></i>';
$html .= $this->renderIcon('delete');
$html .= '</button>';
$html .= '</div>';
}
@@ -938,15 +1144,15 @@ class EntitySelectorRenderer
$methodHelp = $this->getMethodHelpTooltip($includeMethod, $blockType);
$html .= '<span class="method-info-placeholder">';
if (!empty($methodHelp)) {
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '" data-toggle="none">';
$html .= '<i class="material-icons">info</i>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
}
$html .= '</span>';
$html .= '<select class="include-method-select">';
$html .= $this->renderMethodOptions($methods, $includeMethod, false);
$html .= '</select>';
$html .= '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
$html .= '<span class="condition-match-count no-matches">' . $this->renderIcon('visibility') . ' <span class="preview-count">0</span></span>';
$html .= '</div>';
// Value picker
@@ -961,7 +1167,7 @@ class EntitySelectorRenderer
if ($hasExcludes) {
$html .= '<div class="except-separator">';
$html .= '<span class="except-label"><i class="icon-ban"></i> ' . $this->trans('EXCEPT') . '</span>';
$html .= '<span class="except-label">' . $this->renderIcon('block') . ' ' . $this->trans('EXCEPT') . '</span>';
$html .= '</div>';
$html .= '<div class="exclude-rows-container">';
@@ -971,11 +1177,11 @@ class EntitySelectorRenderer
$html .= '</div>';
$html .= '<button type="button" class="btn-add-another-exclude">';
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add another exception');
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add another exception');
$html .= '</button>';
} else {
$html .= '<button type="button" class="btn-add-exclude">';
$html .= '<i class="icon-plus"></i> ' . $this->trans('Add exceptions');
$html .= $this->renderIcon('add') . ' ' . $this->trans('Add exceptions');
$html .= '</button>';
}
@@ -1033,15 +1239,15 @@ class EntitySelectorRenderer
$html .= '<option value="' . $this->escapeAttr($value) . '"' . $selected . '>' . $this->escapeAttr($label) . '</option>';
}
$html .= '</select>';
$sortDirIcon = ($sortDir === 'ASC') ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
$sortDirIcon = ($sortDir === 'ASC') ? 'arrow_upward' : 'arrow_downward';
$html .= '<button type="button" class="btn-sort-dir" data-dir="' . $this->escapeAttr($sortDir) . '" title="' . $this->trans('Sort direction') . '">';
$html .= '<i class="' . $sortDirIcon . '"></i>';
$html .= $this->renderIcon($sortDirIcon);
$html .= '</button>';
$html .= '</span>';
// Preview badge
$html .= '<span class="group-preview-badge clickable" title="' . $this->trans('Preview results') . '">';
$html .= '<i class="icon-eye"></i> <span class="preview-count"></span>';
$html .= $this->renderIcon('visibility') . ' <span class="preview-count"></span>';
$html .= '</span>';
$html .= '</div>';
@@ -1155,8 +1361,8 @@ class EntitySelectorRenderer
$methodHelp = $this->getMethodHelpTooltip($excludeMethod, $blockType);
$html .= '<span class="method-info-placeholder">';
if (!empty($methodHelp)) {
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '" data-toggle="none">';
$html .= '<i class="material-icons">info</i>';
$html .= '<span class="mpr-info-wrapper" data-details="' . $this->escapeAttr($methodHelp) . '">';
$html .= $this->renderIcon('info');
$html .= '</span>';
}
$html .= '</span>';
@@ -1165,11 +1371,11 @@ class EntitySelectorRenderer
$html .= $this->renderMethodOptions($methods, $excludeMethod, true);
$html .= '</select>';
$html .= '<span class="condition-match-count no-matches"><i class="icon-eye"></i> <span class="preview-count">0</span></span>';
$html .= '<span class="condition-match-count no-matches">' . $this->renderIcon('visibility') . ' <span class="preview-count">0</span></span>';
$html .= '</div>'; // End method-selector-wrapper
$html .= '<button type="button" class="btn-remove-exclude-row" title="' . $this->trans('Remove this exception') . '">';
$html .= '<i class="icon-trash"></i>';
$html .= $this->renderIcon('delete');
$html .= '</button>';
$html .= '</div>';
@@ -1215,9 +1421,9 @@ class EntitySelectorRenderer
// Don't pre-wrap chips - JS will create the wrapper with toolbar when chips are added
$html .= '<div class="entity-chips ' . $chipsClass . '" data-placeholder="' . $this->escapeAttr($noItemsPlaceholder) . '"></div>';
$html .= '<div class="entity-search-box">';
$html .= '<i class="icon-search entity-search-icon"></i>';
$html .= $this->renderIcon('search', 'entity-search-icon');
$html .= '<input type="text" class="entity-search-input" placeholder="' . $this->trans('Search by name, reference, ID...') . '" autocomplete="off">';
$html .= '<span class="search-loading" style="display:none;"><i class="icon-spinner icon-spin"></i></span>';
$html .= '<span class="search-loading" style="display:none;">' . $this->renderIcon('sync', 'es-spin') . '</span>';
$html .= '</div>';
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($values)) . '">';
break;
@@ -1229,7 +1435,7 @@ class EntitySelectorRenderer
$html .= '<button type="button" class="btn-toggle-case" title="' . $this->escapeAttr($this->trans('Case insensitive - click to toggle')) . '"><span class="case-icon">aa</span></button>';
$html .= '<input type="text" class="pattern-input" value="" placeholder="' . $this->escapeAttr($this->trans('e.g. *cotton*')) . '">';
$html .= '<span class="pattern-match-count"></span>';
$html .= '<button type="button" class="btn-add-pattern" title="' . $this->escapeAttr($this->trans('Add pattern (Enter)')) . '"><i class="icon-plus"></i></button>';
$html .= '<button type="button" class="btn-add-pattern" title="' . $this->escapeAttr($this->trans('Add pattern (Enter)')) . '">' . $this->renderIcon('add') . '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($values)) . '">';
@@ -1258,7 +1464,7 @@ class EntitySelectorRenderer
$html .= '<input type="number" class="range-min-input" value="" placeholder="' . $this->trans('Min') . '" step="0.01">';
$html .= '<span class="range-separator">-</span>';
$html .= '<input type="number" class="range-max-input" value="" placeholder="' . $this->trans('Max') . '" step="0.01">';
$html .= '<button type="button" class="btn-add-range" title="' . $this->trans('Add range') . '"><i class="icon-plus"></i></button>';
$html .= '<button type="button" class="btn-add-range" title="' . $this->trans('Add range') . '">' . $this->renderIcon('add') . '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '<input type="hidden" class="' . $dataClass . '" value="' . $this->escapeAttr(json_encode($ranges)) . '">';
@@ -1275,7 +1481,7 @@ class EntitySelectorRenderer
$optIcon = is_array($optData) ? ($optData['icon'] ?? '') : '';
$html .= '<button type="button" class="tile-option' . $selectedClass . '" data-value="' . $this->escapeAttr($optKey) . '">';
if ($optIcon) {
$html .= '<i class="' . $this->escapeAttr($optIcon) . '"></i> ';
$html .= $this->renderIcon($optIcon) . ' ';
}
$html .= '<span class="tile-label">' . $this->escapeAttr($optLabel) . '</span>';
$html .= '</button>';
@@ -1546,7 +1752,6 @@ class EntitySelectorRenderer
],
'methodHelp' => $this->getAllMethodHelpContent(),
'combinationMode' => $config['combination_mode'] ?? 'products',
'productSelectionLevel' => $config['product_selection_level'] ?? 'product',
'emptyMeansAll' => $config['empty_means_all'] ?? true,
];
}
@@ -1582,23 +1787,23 @@ class EntitySelectorRenderer
switch ($sortBy) {
case 'name':
return $isAsc ? 'icon-sort-alpha-asc' : 'icon-sort-alpha-desc';
return $isAsc ? 'sort_by_alpha' : 'sort_by_alpha';
case 'price':
case 'quantity':
case 'product_count':
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
return $isAsc ? 'sort' : 'sort';
case 'date_add':
case 'newest_products':
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
return $isAsc ? 'sort' : 'sort';
case 'sales':
case 'total_sales':
return $isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
return $isAsc ? 'arrow_upward' : 'arrow_downward';
case 'position':
return $isAsc ? 'icon-sort-numeric-asc' : 'icon-sort-numeric-desc';
return $isAsc ? 'sort' : 'sort';
case 'random':
return 'icon-random';
return 'shuffle';
default:
return $isAsc ? 'icon-sort-amount-asc' : 'icon-sort-amount-desc';
return $isAsc ? 'arrow_upward' : 'arrow_downward';
}
}
}

View File

@@ -855,8 +855,8 @@ trait ScheduleConditions
$html .= '<button type="button" class="btn-add-group">';
$html .= '<i class="icon-plus"></i> ' . $this->transScheduleConditions('Add selection group');
$html .= '</button>';
$html .= '<span class="mpr-info-wrapper" data-details="' . htmlspecialchars($groupsTooltip) . '" data-toggle="none">';
$html .= '<i class="material-icons">info</i>';
$html .= '<span class="mpr-info-wrapper" data-details="' . htmlspecialchars($groupsTooltip) . '">';
$html .= '<i class="icon-question-sign"></i>';
$html .= '</span>';
$html .= '</div>';