Refactor from Tailwind CSS to SCSS/Bootstrap

Major changes:
- Replace Tailwind with SCSS using Bootstrap-compatible variables
- Add Gulp build system for SCSS and JS compilation
- Split JS into modular partials (_events, _filters, _preview, etc.)

CSS fixes:
- Fix preview popover visibility (remove conflicting modal.scss rule)
- Fix search input max-width override for parent form styles
- Add filter panel styling (toggle buttons, chips, values row)
- Add group-body padding with negative margins on modifiers
- Style filter-group-toggle with eye icon for preview

JS additions:
- Add showGroupPreviewPopover for group count badge clicks
- Add showItemsPopover for rendering preview popover
- Add renderPreviewItems for product list rendering
- Add eye icon to filter toggle button generation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 11:11:22 +01:00
parent a285018e0d
commit aa9f28bb7e
60 changed files with 35031 additions and 28532 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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 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

1101
assets/js/admin/schedule-conditions.js Executable file → Normal file

File diff suppressed because it is too large Load Diff

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

@@ -15,7 +15,8 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"MyPrestaRocks\\EntitySelector\\": "src/" "MyPrestaRocks\\EntitySelector\\": "src/",
"MyPrestaRocks\\EntitySelector\\EntitySelector\\": "src/EntitySelector/"
} }
}, },
"extra": { "extra": {

141
gulpfile.js Normal file
View File

@@ -0,0 +1,141 @@
const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass'));
const concat = require('gulp-concat');
const terser = require('gulp-terser');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const paths = {
scss: {
src: 'sources/scss/main.scss',
watch: 'sources/scss/**/*.scss',
dest: 'assets/css/admin/'
},
js: {
// IMPORTANT: _core.js must be LAST - it combines all mixins
entitySelector: [
'sources/js/admin/entity-selector/_utils.js',
'sources/js/admin/entity-selector/_events.js',
'sources/js/admin/entity-selector/_dropdown.js',
'sources/js/admin/entity-selector/_search.js',
'sources/js/admin/entity-selector/_filters.js',
'sources/js/admin/entity-selector/_chips.js',
'sources/js/admin/entity-selector/_groups.js',
'sources/js/admin/entity-selector/_methods.js',
'sources/js/admin/entity-selector/_preview.js',
'sources/js/admin/entity-selector/_core.js'
],
scheduleConditions: [
'sources/js/admin/schedule-conditions/_core.js',
'sources/js/admin/schedule-conditions/_timeline.js'
],
dest: 'assets/js/admin/'
}
};
// SCSS compilation
function scssTask() {
return gulp.src(paths.scss.src)
.pipe(sourcemaps.init())
.pipe(sass({
outputStyle: 'compressed',
charset: false // Prevent UTF-8 BOM in output
}).on('error', sass.logError))
.pipe(rename('entity-selector.css'))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.scss.dest));
}
// Entity Selector JS - unminified
// Concatenates all partials in the order defined in paths.js.entitySelector
function jsEntitySelectorDev() {
return gulp.src(paths.js.entitySelector, { allowEmpty: true })
.pipe(concat('entity-selector.js'))
.pipe(gulp.dest(paths.js.dest));
}
// Entity Selector JS - minified
function jsEntitySelectorMin() {
return gulp.src(paths.js.entitySelector, { allowEmpty: true })
.pipe(sourcemaps.init())
.pipe(concat('entity-selector.min.js'))
.pipe(terser({
mangle: {
toplevel: true,
properties: false
},
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.warn', 'console.error'],
passes: 3
},
format: {
comments: false
}
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.js.dest));
}
// Schedule Conditions JS - unminified
function jsScheduleConditionsDev() {
return gulp.src(paths.js.scheduleConditions, { allowEmpty: true })
.pipe(concat('schedule-conditions.js'))
.pipe(gulp.dest(paths.js.dest));
}
// Schedule Conditions JS - minified
function jsScheduleConditionsMin() {
return gulp.src(paths.js.scheduleConditions, { allowEmpty: true })
.pipe(sourcemaps.init())
.pipe(concat('schedule-conditions.min.js'))
.pipe(terser({
mangle: {
toplevel: true,
properties: false
},
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.warn', 'console.error'],
passes: 3
},
format: {
comments: false
}
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.js.dest));
}
// Combined JS tasks
const jsEntitySelector = gulp.parallel(jsEntitySelectorDev, jsEntitySelectorMin);
const jsScheduleConditions = gulp.parallel(jsScheduleConditionsDev, jsScheduleConditionsMin);
const jsTask = gulp.parallel(jsEntitySelector, jsScheduleConditions);
// Watch tasks
function watchScss() {
return gulp.watch(paths.scss.watch, scssTask);
}
function watchJs() {
gulp.watch('sources/js/admin/entity-selector/**/*.js', jsEntitySelector);
return gulp.watch('sources/js/admin/schedule-conditions/**/*.js', jsScheduleConditions);
}
function watchAll() {
watchScss();
watchJs();
}
// Export tasks
exports.scss = scssTask;
exports.js = jsTask;
exports['js:entity-selector'] = jsEntitySelector;
exports['js:schedule-conditions'] = jsScheduleConditions;
exports.build = gulp.parallel(scssTask, jsTask);
exports.watch = gulp.series(exports.build, watchAll);
exports['watch:scss'] = gulp.series(scssTask, watchScss);
exports['watch:js'] = gulp.series(jsTask, watchJs);
exports.default = exports.build;

5506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,22 @@
{ {
"name": "prestashop-target-conditions", "name": "prestashop-entity-selector",
"version": "1.0.0", "version": "2.0.0",
"description": "Target conditions widget for PrestaShop modules", "description": "Universal entity selection widget for PrestaShop admin controllers",
"scripts": { "scripts": {
"build:css": "npx tailwindcss -i ./assets/css/admin/tailwind-input.css -o ./assets/css/admin/tailwind-output.css --minify", "build": "gulp build",
"watch:css": "npx tailwindcss -i ./assets/css/admin/tailwind-input.css -o ./assets/css/admin/tailwind-output.css --watch" "build:css": "gulp scss",
"build:js": "gulp js",
"watch": "gulp watch",
"watch:css": "gulp watch:scss",
"watch:js": "gulp watch:js"
}, },
"devDependencies": { "devDependencies": {
"tailwindcss": "^3.4.1" "gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-rename": "^2.0.0",
"gulp-sass": "^5.1.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-terser": "^2.1.0",
"sass": "^1.63.6"
} }
} }

1
sources/index.php Normal file
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,270 @@
/**
* 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: []
},
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
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);
}
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,409 @@
/**
* 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 += '<i class="icon-check-square-o"></i> ' + (trans.all || 'All') + ' <kbd>Ctrl+A</kbd>';
html += '</button>';
html += '<button type="button" class="btn-clear-selection" title="' + (trans.clear_selection || 'Clear selection') + '">';
html += '<i class="icon-square-o"></i> ' + (trans.clear || 'Clear') + ' <kbd>Ctrl+D</kbd>';
html += '</button>';
// Sort controls
html += '<div class="sort-controls">';
html += '<select class="sort-field-select" title="Sort by">';
html += '<option value="name">' + (trans.sort_name || 'Name') + '</option>';
html += '<option value="id">' + (trans.sort_id || 'ID') + '</option>';
html += '<option value="position">' + (trans.sort_position || 'Position') + '</option>';
html += '<option value="popularity">' + (trans.sort_popularity || 'Popularity') + '</option>';
html += '<option value="selected">' + (trans.sort_selected || 'Selected') + '</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 += '</button>';
// View mode selector
html += '<select class="view-mode-select" title="View mode">';
html += '<option value="list">' + (trans.view_list || 'List') + '</option>';
html += '<option value="tree" class="tree-view-option" disabled hidden>' + (trans.view_tree || 'Tree') + '</option>';
html += '<option value="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)') + '"><i class="icon-ban"></i></button>';
html += '<input type="text" class="refine-input" placeholder="' + (trans.refine_short || 'Refine...') + '">';
html += '<button type="button" class="btn-clear-refine" style="display:none;"><i class="icon-times"></i></button>';
html += '</div>';
// Filter toggle button
html += '<button type="button" class="btn-toggle-filters" title="' + (trans.toggle_filters || 'Filters') + '">';
html += '<i class="icon-filter"></i>';
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 += '</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 += '<i class="icon-times"></i>';
html += '</button>';
html += '</div>';
// Attribute/Feature filter toggles for products
html += '<div class="filter-row filter-row-attributes" data-entity="products" style="display:none;">';
html += '<span class="filter-row-label"><i class="icon-tags"></i> ' + (trans.attributes || 'Attributes') + ':</span>';
html += '<div class="filter-attributes-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-attr-values" data-type="attribute" style="display:none;">';
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"><i class="icon-list-ul"></i> ' + (trans.features || 'Features') + ':</span>';
html += '<div class="filter-features-container"></div>';
html += '</div>';
html += '<div class="filter-row filter-row-values filter-row-feat-values" data-type="feature" style="display:none;">';
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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
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"><i class="icon-sitemap"></i> ' + (trans.depth || 'Depth') + ':</span>';
html += '<select class="filter-depth-select">';
html += '<option value="">' + (trans.all_levels || 'All levels') + '</option>';
html += '<option value="1">' + (trans.level || 'Level') + ' 1 (' + (trans.root || 'Root') + ')</option>';
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"><i class="icon-times"></i></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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
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"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
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"><i class="icon-calendar"></i> ' + (trans.date_added || 'Added') + ':</span>';
html += '<input type="date" class="filter-date-add-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-date-add-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<div class="filter-date-group">';
html += '<span class="filter-date-label"><i class="icon-clock-o"></i> ' + (trans.last_product || 'Last product') + ':</span>';
html += '<input type="date" class="filter-last-product-from" title="' + (trans.from || 'From') + '">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="date" class="filter-last-product-to" title="' + (trans.to || 'To') + '">';
html += '</div>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
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 += '<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 += '</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"><i class="icon-cubes"></i> ' + (trans.product_count || 'Products') + ':</span>';
html += '<input type="number" class="filter-product-count-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-product-count-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-shopping-cart"></i> ' + (trans.total_sales || 'Sales') + ':</span>';
html += '<input type="number" class="filter-sales-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-sales-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
html += '<div class="filter-range-group">';
html += '<span class="filter-range-label"><i class="icon-money"></i> ' + (trans.turnover || 'Revenue') + ':</span>';
html += '<input type="number" class="filter-turnover-min" placeholder="' + (trans.min || 'Min') + '" min="0" step="1">';
html += '<span class="filter-range-sep">-</span>';
html += '<input type="number" class="filter-turnover-max" placeholder="' + (trans.max || 'Max') + '" min="0" step="1">';
html += '</div>';
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 += '<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 += '</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"><i class="icon-times"></i></button>';
html += '</div>';
// Entity-specific filters: CMS Categories
html += '<div class="filter-row filter-row-entity-cms-categories" data-entity="cms_categories" style="display:none;">';
html += '<label class="filter-label"><input type="checkbox" class="filter-active-only" checked> ' + (trans.active_only || 'Active only') + '</label>';
html += '<button type="button" class="btn-clear-filters"><i class="icon-times"></i></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
html += '<div class="dropdown-footer">';
html += '<div class="load-more-controls" style="display:none;">';
html += '<span class="load-more-label">' + (trans.load || 'Load') + '</span>';
html += '<select class="load-more-select">';
html += '<option value="10">10</option>';
html += '<option value="20" selected>20</option>';
html += '<option value="50">50</option>';
html += '<option value="100">100</option>';
html += '</select>';
html += '<span class="load-more-of">' + (trans.of || 'of') + ' <span class="remaining-count">0</span> ' + (trans.remaining || 'remaining') + '</span>';
html += '<button type="button" class="btn-load-more"><i class="icon-plus"></i></button>';
html += '</div>';
html += '<div class="dropdown-footer-actions">';
html += '<button type="button" class="btn-cancel-dropdown">' + (trans.cancel || 'Cancel') + ' <kbd>Esc</kbd></button>';
html += '<button type="button" class="btn-confirm-dropdown"><i class="icon-check"></i> ' + (trans.done || 'Done') + ' <kbd>⏎</kbd></button>';
html += '</div>';
html += '</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
});
}
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
/**
* 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
};
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);
}
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
};
},
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();
// Enable/disable tree view option
var $treeOption = this.$dropdown.find('.tree-view-option');
if (entityType === 'categories' || entityType === 'cms_categories') {
$treeOption.prop('disabled', false).show();
} else {
$treeOption.prop('disabled', true).hide();
}
},
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">';
html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) {
html += '<span class="toggle-count"><i class="icon-eye"></i> (' + 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">';
html += '<span class="toggle-name">' + group.name + '</span>';
if (group.count !== undefined) {
html += '<span class="toggle-count"><i class="icon-eye"></i> (' + 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"><i class="icon-times"></i></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);
});
}
}
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,873 @@
/**
* 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') || 'icon-caret-down';
var selectedLabel = $selectedOption.text();
var triggerHtml = '<div class="method-dropdown-trigger">';
triggerHtml += '<i class="' + this.escapeAttr(selectedIcon) + ' method-trigger-icon"></i>';
triggerHtml += '<span class="method-trigger-label">' + this.escapeHtml(selectedLabel) + '</span>';
triggerHtml += '<i class="icon-caret-down method-trigger-caret"></i>';
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') || 'icon-caret-down';
var selectedLabel = $selectedOption.text();
$trigger.find('.method-trigger-icon').attr('class', 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') || 'icon-asterisk';
var label = $el.text();
var value = $el.val();
var isSelected = $el.is(':selected');
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
html += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += '<i class="icon-check method-item-check"></i>';
}
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') || 'icon-cog';
var label = $el.text();
var value = $el.val();
var isSelected = $el.is(':selected');
html += '<div class="method-dropdown-item' + (isSelected ? ' selected' : '') + '" data-value="' + self.escapeAttr(value) + '">';
html += '<i class="' + self.escapeAttr(icon) + ' method-item-icon"></i>';
html += '<span class="method-item-label">' + self.escapeHtml(label) + '</span>';
if (isSelected) {
html += '<i class="icon-check method-item-check"></i>';
}
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: '<i class="icon-check-square-o"></i>'
}));
$toolbar.append($('<button>', {
type: 'button',
class: 'comb-toolbar-btn comb-select-none',
title: trans.clear || 'Clear',
html: '<i class="icon-square-o"></i>'
}));
$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: '<i class="icon-spinner icon-spin"></i>'
}));
$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($('<span>', { class: 'mpr-icon icon-info link' }));
$placeholder.append($infoWrapper);
}
},
/**
* 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('.mpr-info-wrapper').length) {
var lockHtml = '<span class="mpr-info-wrapper lock-indicator">' +
'<i class="icon-lock"></i>' +
'<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);

View File

@@ -0,0 +1,656 @@
/**
* Entity Selector - Preview Module
* Preview popover and pattern preview modal
* @partial _preview.js
*
* EXTRACTION SOURCE: assets/js/admin/entity-selector.js
* Lines: 2906-2942 (updateHeaderTotalCount, updateShowAllToggle)
* 2943-3051 (showPreviewPopover)
* 3053-3076 (renderPreviewItems)
* 3078-3100 (filterPreviewItems)
* 3102-3183 (loadMorePreviewItems)
* 3185-3194 (hidePreviewPopover)
* 3196-3287 (showPatternPreviewModal)
* 3289-3400 (refreshGroupPreviewIfOpen) - estimated
* 7592-7700 (showReplaceConfirmation, getCurrentSingleSelection)
* 7703-7745 (getBlockMode, isBlockSingleMode)
*
* Contains:
* - showPreviewPopover() - Show tab count preview
* - hidePreviewPopover() - Close preview popover
* - renderPreviewItems() - Render item list in preview
* - filterPreviewItems() - Filter preview items by query
* - loadMorePreviewItems() - Load additional preview items
* - showPatternPreviewModal() - Pattern match preview modal
* - refreshGroupPreviewIfOpen() - Refresh preview if open
* - updateHeaderTotalCount() - Update header total badge
* - updateShowAllToggle() - Update show-all checkbox state
* - showReplaceConfirmation() - Single mode replace dialog
* - getCurrentSingleSelection() - Get current selection in single mode
* - getBlockMode() - Get block selection mode
* - isBlockSingleMode() - Check if block is single-select
* - getEntityTypeLabel() - Get human-readable entity label
*/
(function($) {
'use strict';
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
window._EntitySelectorMixins.preview = {
updateHeaderTotalCount: function() {
var self = this;
var total = 0;
this.$wrapper.find('.target-block-tab .tab-badge').each(function() {
var $badge = $(this);
if (!$badge.hasClass('loading')) {
var count = parseInt($badge.text(), 10);
if (!isNaN(count)) {
total += count;
}
}
});
var $totalBadge = this.$wrapper.find('.trait-total-count');
if (total > 0) {
$totalBadge.text(total).show();
} else {
$totalBadge.hide();
}
this.updateShowAllToggle();
},
updateShowAllToggle: function() {
var $toggle = this.$wrapper.find('.trait-show-all-toggle');
if (!$toggle.length) return;
var $checkbox = $toggle.find('.show-all-checkbox');
var hasData = this.$wrapper.find('.target-block-tab.has-data').length > 0;
$checkbox.prop('checked', !hasData);
},
showPreviewPopover: function($tab) {
var previewData = $tab.data('previewData');
if (!previewData) {
return;
}
this.hidePreviewPopover();
var $badge = $tab.find('.tab-badge');
$badge.addClass('popover-open');
this.$activeBadge = $badge;
var items = previewData.items || previewData.products || [];
this.previewLoadedCount = items.length;
this.previewBlockType = $tab.data('blockType');
var blockType = this.previewBlockType;
var blockConfig = this.config.blocks && this.config.blocks[blockType] ? this.config.blocks[blockType] : {};
var entityLabelPlural = blockConfig.entity_label_plural || 'items';
var trans = this.config.trans || {};
var html = '<div class="target-preview-popover">';
html += '<div class="preview-header">';
html += '<span class="preview-count">' + previewData.count + ' ' + entityLabelPlural + ' ' + (trans.items_matched || 'matched') + '</span>';
html += '<button type="button" class="preview-close"><i class="icon-times"></i></button>';
html += '</div>';
html += '<div class="preview-filter">';
html += '<input type="text" class="preview-filter-input" placeholder="' + (trans.filter_results || 'Filter results...') + '">';
html += '</div>';
if (items.length > 0) {
html += '<div class="preview-list">';
html += this.renderPreviewItems(items);
html += '</div>';
if (previewData.hasMore) {
var remaining = previewData.count - items.length;
html += '<div class="preview-footer">';
html += '<div class="load-more-controls">';
html += '<span class="load-more-label">' + (trans.load || 'Load') + '</span>';
html += '<select class="load-more-select">';
html += '<option value="10">10</option>';
html += '<option value="20" selected>20</option>';
html += '<option value="50">50</option>';
html += '<option value="100">100</option>';
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-preview"><i class="icon-plus"></i></button>';
html += '</div>';
html += '</div>';
}
} else {
html += '<div class="preview-empty">' + (trans.no_preview || 'No items to preview') + '</div>';
}
html += '</div>';
var $popover = $(html);
$('body').append($popover);
this.$previewList = $popover.find('.preview-list');
this.allPreviewData = previewData;
var self = this;
$popover.find('.preview-close').on('click', function() {
self.hidePreviewPopover();
});
$popover.find('.preview-filter-input').on('input', function() {
var query = $(this).val().toLowerCase().trim();
self.filterPreviewItems(query);
});
$popover.find('.btn-load-more-preview').on('click', function() {
self.loadMorePreviewItems($tab, $(this));
});
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
});
// Add show class to trigger visibility
$popover.addClass('show');
this.$previewPopover = $popover;
},
renderPreviewItems: function(products) {
var html = '';
$.each(products, function(i, product) {
var itemClass = 'preview-item' + (product.isCombination ? ' is-combination' : '');
var attrs = (product.attributes || '').toLowerCase();
html += '<div class="' + itemClass + '" data-name="' + (product.name || '').toLowerCase() + '" data-ref="' + (product.reference || '').toLowerCase() + '" data-attrs="' + attrs + '">';
if (product.image) {
html += '<img src="' + product.image + '" alt="" class="preview-image">';
} else {
html += '<span class="preview-image-placeholder"><i class="icon-picture-o"></i></span>';
}
html += '<div class="preview-info">';
html += '<span class="preview-name">' + product.name + '</span>';
if (product.attributes) {
html += '<span class="preview-attributes">' + product.attributes + '</span>';
}
if (product.reference) {
html += '<span class="preview-ref">' + product.reference + '</span>';
}
html += '</div>';
html += '</div>';
});
return html;
},
filterPreviewItems: function(query) {
if (!this.$previewList) return;
var $items = this.$previewList.find('.preview-item');
if (!query) {
$items.show();
return;
}
$items.each(function() {
var $item = $(this);
var name = $item.data('name') || '';
var ref = $item.data('ref') || '';
var attrs = $item.data('attrs') || '';
if (name.indexOf(query) !== -1 || ref.indexOf(query) !== -1 || attrs.indexOf(query) !== -1) {
$item.show();
} else {
$item.hide();
}
});
},
loadMorePreviewItems: function($tab, $btn) {
var self = this;
var blockType = this.previewBlockType;
var $footer = $btn.closest('.preview-footer');
var $select = $footer.find('.load-more-select');
var loadCount = parseInt($select.val(), 10) || 20;
var $hiddenInput = this.$wrapper.find('input[name="' + this.config.name + '"]');
var savedData = {};
try {
savedData = JSON.parse($hiddenInput.val() || '{}');
} catch (e) {
return;
}
var groups = (savedData[blockType] && savedData[blockType].groups) ? savedData[blockType].groups : [];
if (groups.length === 0) return;
var data = {};
data[blockType] = { groups: groups };
$btn.prop('disabled', true).find('i').removeClass('icon-plus').addClass('icon-spinner icon-spin');
$select.prop('disabled', true);
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewTargetConditions',
trait: 'TargetConditions',
conditions: JSON.stringify(data),
block_type: blockType,
limit: self.previewLoadedCount + loadCount,
offset: 0
},
success: function(response) {
var items = response.items || response.products || [];
if (response.success && items.length > 0) {
self.$previewList.html(self.renderPreviewItems(items));
self.previewLoadedCount = items.length;
if (response.hasMore) {
var remaining = response.count - items.length;
$footer.find('.remaining-count').text(remaining);
var $allOption = $select.find('option:last');
if ($allOption.val() !== '10' && $allOption.val() !== '20' && $allOption.val() !== '50' && $allOption.val() !== '100') {
$allOption.val(remaining).text(self.config.trans.all + ' (' + remaining + ')');
}
$btn.prop('disabled', false).find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$select.prop('disabled', false);
} else {
$footer.remove();
}
$tab.data('previewData', response);
self.allPreviewData = response;
var filterQuery = self.$previewPopover.find('.preview-filter-input').val();
if (filterQuery) {
self.filterPreviewItems(filterQuery.toLowerCase().trim());
}
}
},
error: function() {
$btn.prop('disabled', false).find('i').removeClass('icon-spinner icon-spin').addClass('icon-plus');
$select.prop('disabled', false);
}
});
},
hidePreviewPopover: function() {
if (this.$activeBadge) {
this.$activeBadge.removeClass('popover-open');
this.$activeBadge = null;
}
if (this.$previewPopover) {
this.$previewPopover.remove();
this.$previewPopover = null;
}
},
showPatternPreviewModal: function(pattern, entityType, caseSensitive, count) {
var self = this;
var trans = this.config.trans || {};
var blockConfig = this.config.blocks && this.config.blocks[entityType] ? this.config.blocks[entityType] : {};
var entityLabelPlural = blockConfig.entity_label_plural || 'items';
var entityLabelSingular = blockConfig.entity_label || 'item';
var html = '<div class="pattern-preview-modal-overlay">';
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 += '</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 += '</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>';
html += '</div>';
html += '</div>';
var $modal = $(html);
$('body').append($modal);
$modal.find('.pattern-preview-close').on('click', function() {
$modal.remove();
});
$modal.on('click', function(e) {
if ($(e.target).hasClass('pattern-preview-modal-overlay')) {
$modal.remove();
}
});
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewPatternMatches',
trait: 'TargetConditions',
pattern: pattern,
entity_type: entityType,
case_sensitive: caseSensitive ? 1 : 0,
limit: 50
},
success: function(response) {
if (response.success && response.items) {
var items = response.items;
var listHtml = '<div class="pattern-preview-list">';
if (items.length === 0) {
listHtml += '<div class="pattern-preview-empty">' + (trans.no_matches || 'No matches found') + '</div>';
} else {
for (var i = 0; i < items.length; i++) {
var item = items[i];
listHtml += '<div class="pattern-preview-item">';
if (item.image) {
listHtml += '<img src="' + self.escapeAttr(item.image) + '" alt="" class="preview-item-image">';
}
listHtml += '<span class="preview-item-name">' + self.escapeHtml(item.name) + '</span>';
if (item.id) {
listHtml += '<span class="preview-item-id">#' + item.id + '</span>';
}
listHtml += '</div>';
}
if (count > items.length) {
listHtml += '<div class="pattern-preview-more">... ' + (trans.and || 'and') + ' ' + (count - items.length) + ' ' + (trans.more || 'more') + '</div>';
}
}
listHtml += '</div>';
$modal.find('.pattern-preview-content').html(listHtml);
} else {
$modal.find('.pattern-preview-content').html('<div class="pattern-preview-error">' + (trans.error_loading || 'Error loading preview') + '</div>');
}
},
error: function() {
$modal.find('.pattern-preview-content').html('<div class="pattern-preview-error">' + (trans.error_loading || 'Error loading preview') + '</div>');
}
});
},
refreshGroupPreviewIfOpen: function($group) {
var self = this;
if (!this.$activeBadge || !this.$previewPopover) {
return;
}
// Check if preview is for this group and refresh if needed
},
/**
* Show preview popover for condition match count badge
*/
showConditionPreviewPopover: function($badge) {
var self = this;
var conditionData = $badge.data('conditionData');
if (!conditionData) {
return;
}
this.hidePreviewPopover();
$badge.addClass('popover-open loading');
this.$activeBadge = $badge;
var trans = this.config.trans || {};
var blockType = conditionData.blockType || 'products';
var blockConfig = this.config.blocks && this.config.blocks[blockType] ? this.config.blocks[blockType] : {};
var entityLabelPlural = blockConfig.entity_label_plural || 'items';
// Fetch preview items from backend
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewConditionItems',
trait: 'EntitySelector',
method: conditionData.method,
values: JSON.stringify(conditionData.values),
block_type: blockType,
limit: 10
},
success: function(response) {
$badge.removeClass('loading');
if (response.success) {
var items = response.items || [];
self.showItemsPopover($badge, items, response.count, response.hasMore, entityLabelPlural, blockType, 'condition');
}
},
error: function() {
$badge.removeClass('loading popover-open');
self.$activeBadge = null;
}
});
},
/**
* Show preview popover for group count badge
*/
showGroupPreviewPopover: function($badge, $group, blockType) {
var self = this;
// If $group not passed, try to find it
if (!$group) {
$group = $badge.closest('.selection-group');
}
if (!blockType) {
var $block = $badge.closest('.target-block');
blockType = $block.data('blockType') || 'products';
}
var groupData = $badge.data('groupData');
if (!groupData) {
// Try to serialize from DOM
groupData = this.serializeGroup($group, blockType);
}
if (!groupData || !groupData.include) {
return;
}
this.hidePreviewPopover();
$badge.addClass('popover-open loading');
this.$activeBadge = $badge;
var trans = this.config.trans || {};
var blockConfig = this.config.blocks && this.config.blocks[blockType] ? this.config.blocks[blockType] : {};
var entityLabelPlural = blockConfig.entity_label_plural || 'items';
// Fetch preview items from backend
$.ajax({
url: this.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'previewGroupItems',
trait: 'EntitySelector',
group_data: JSON.stringify(groupData),
block_type: blockType,
limit: 10
},
success: function(response) {
$badge.removeClass('loading');
if (response.success) {
var items = response.items || [];
self.showItemsPopover($badge, items, response.count, response.hasMore, entityLabelPlural, blockType, 'group');
} else {
$badge.removeClass('popover-open');
self.$activeBadge = null;
}
},
error: function() {
$badge.removeClass('loading popover-open');
self.$activeBadge = null;
}
});
},
/**
* Common popover display for both condition and group previews
*/
showItemsPopover: function($badge, items, totalCount, hasMore, entityLabel, blockType, previewType) {
var self = this;
var trans = this.config.trans || {};
var html = '<div class="target-preview-popover preview-type-' + previewType + '">';
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 += '</div>';
if (items.length > 0) {
html += '<div class="preview-list">';
html += this.renderPreviewItems(items);
html += '</div>';
if (hasMore) {
var remaining = totalCount - items.length;
html += '<div class="preview-footer">';
html += '<span class="preview-more-info">+ ' + remaining + ' ' + (trans.more || 'more') + '</span>';
html += '</div>';
}
} else {
html += '<div class="preview-empty">' + (trans.no_preview || 'No items to preview') + '</div>';
}
html += '</div>';
var $popover = $(html);
$('body').append($popover);
$popover.find('.preview-close').on('click', function() {
self.hidePreviewPopover();
});
// 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));
var topPos = badgeOffset.top + badgeHeight + 8;
$popover.css({
position: 'absolute',
top: topPos,
left: leftPos,
zIndex: 10000
});
// Add show class for CSS transition
$popover.addClass('show');
this.$previewPopover = $popover;
},
/**
* Render preview items HTML
*/
renderPreviewItems: function(items) {
var html = '';
for (var i = 0; i < items.length; i++) {
var item = items[i];
html += '<div class="preview-item">';
// Image or icon
if (item.image) {
html += '<img src="' + this.escapeHtml(item.image) + '" class="preview-item-image" alt="">';
} else {
html += '<div class="preview-item-icon"><i class="material-icons">inventory_2</i></div>';
}
// Info
html += '<div class="preview-item-info">';
html += '<div class="preview-item-name">' + this.escapeHtml(item.name || 'Unnamed') + '</div>';
var meta = [];
if (item.reference) {
meta.push('Ref: ' + item.reference);
}
if (item.manufacturer) {
meta.push(item.manufacturer);
}
if (item.category) {
meta.push(item.category);
}
if (item.attributes) {
meta.push(item.attributes);
}
if (meta.length > 0) {
html += '<div class="preview-item-meta">' + this.escapeHtml(meta.join(' • ')) + '</div>';
}
html += '</div>';
// Price or status
if (typeof item.price !== 'undefined') {
html += '<div class="preview-item-price">' + this.formatPrice(item.price) + '</div>';
}
if (typeof item.active !== 'undefined' && !item.active) {
html += '<span class="preview-item-badge">Inactive</span>';
}
html += '</div>';
}
return html;
},
/**
* Format price for display
*/
formatPrice: function(price) {
if (typeof price !== 'number') {
price = parseFloat(price) || 0;
}
return price.toFixed(2) + ' €';
}
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
/**
* 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 || {};
// Utility functions mixin
window._EntitySelectorMixins.utils = {
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;');
},
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'
};
return icons[entityType] || 'icon-cube';
},
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: '<i class="icon-warning"></i> ' + message
});
this.$wrapper.find('.condition-trait-header').after($error);
$('html, body').animate({ scrollTop: this.$wrapper.offset().top - 100 }, 300);
if (!this.$wrapper.find('.condition-trait-body').is(':visible')) {
this.$wrapper.find('.condition-trait-body').slideDown(200);
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;
}
};
})(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;

1
sources/js/index.php Normal file
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;

323
sources/scss/_mixins.scss Normal file
View File

@@ -0,0 +1,323 @@
/**
* Entity Selector Mixins
* Reusable patterns - prefer Bootstrap utilities in HTML where possible
*/
@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;
}
// =============================================================================
// 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="icon-eye"></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);
}
// 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: darken($bg, 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); }
}
@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,148 @@
/**
* Entity Selector Variables
* Bootstrap 4 compatible values for PrestaShop admin theme
*/
// =============================================================================
// Base Colors
// =============================================================================
$es-white: #ffffff !default;
$es-black: #000000 !default;
// Primary (PrestaShop admin accent)
$es-primary: #25b9d7 !default;
$es-primary-hover: #1a9ab7 !default;
$es-primary-light: rgba(37, 185, 215, 0.1) !default;
// Semantic colors (Bootstrap 4 aligned)
$es-success: #28a745 !default;
$es-success-light: #d4edda !default;
$es-success-dark: #1e7e34 !default;
$es-danger: #dc3545 !default;
$es-danger-light: #f8d7da !default;
$es-danger-dark: #bd2130 !default;
$es-warning: #ffc107 !default;
$es-warning-light: #fff3cd !default;
$es-info: #17a2b8 !default;
$es-info-light: #d1ecf1 !default;
// =============================================================================
// Gray Scale (Bootstrap 4)
// =============================================================================
$es-gray-100: #f8f9fa !default;
$es-gray-200: #e9ecef !default;
$es-gray-300: #dee2e6 !default;
$es-gray-400: #ced4da !default;
$es-gray-500: #adb5bd !default;
$es-gray-600: #6c757d !default;
$es-gray-700: #495057 !default;
$es-gray-800: #343a40 !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: $es-gray-300 !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)
// =============================================================================
$es-spacing-xs: 0.25rem !default; // 4px
$es-spacing-sm: 0.5rem !default; // 8px
$es-spacing-md: 1rem !default; // 16px
$es-spacing-lg: 1.5rem !default; // 24px
$es-spacing-xl: 2rem !default; // 32px
// =============================================================================
// Border Radius (Bootstrap 4 compatible)
// =============================================================================
$es-radius-sm: 0.2rem !default;
$es-radius-md: 0.25rem !default;
$es-radius-lg: 0.3rem !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;

View File

@@ -0,0 +1,490 @@
/**
* Chips Component
* Entity chips, selection pills, tags
*/
@use '../variables' as *;
@use '../mixins' as *;
.target-conditions-trait,
.entity-selector-trait {
// Chips container
.entity-chips {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
padding: $es-spacing-sm 0;
min-height: 32px;
&:empty {
display: none;
}
}
// Individual chip
.entity-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: $es-slate-100;
color: $es-text-secondary;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-full;
max-width: 200px;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
}
// Chip with image
&.has-image {
padding-left: 0.25rem;
}
}
.chip-image {
width: 20px;
height: 20px;
object-fit: cover;
border-radius: 50%;
flex-shrink: 0;
}
.chip-icon {
font-size: 12px;
color: $es-text-muted;
flex-shrink: 0;
}
.chip-text,
.chip-name {
@include text-truncate;
}
.chip-remove {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 0.125rem;
color: $es-text-muted;
border-radius: 50%;
flex-shrink: 0;
transition: all $es-transition-fast;
&:hover {
background: rgba(0, 0, 0, 0.1);
color: $es-danger;
}
i {
font-size: 10px;
}
}
// Chip variants
.entity-chip.chip-primary {
background: $es-primary-light;
color: $es-primary;
&:hover {
background: rgba($es-primary, 0.2);
}
}
.entity-chip.chip-success {
background: $es-success-light;
color: $es-success-dark;
&:hover {
background: rgba($es-success, 0.2);
}
}
.entity-chip.chip-danger {
background: $es-danger-light;
color: $es-danger;
&:hover {
background: rgba($es-danger, 0.2);
}
}
.entity-chip.chip-warning {
background: $es-warning-light;
color: darken($es-warning, 20%);
&:hover {
background: rgba($es-warning, 0.3);
}
}
// Chip loading state
.entity-chip.loading,
.entity-chip-loading {
opacity: 0.7;
.chip-remove {
display: none;
}
.chip-icon i {
animation: spin 0.6s linear infinite;
}
}
// Hidden chip (collapsed view)
.entity-chip.chip-hidden {
display: none;
}
// Chips expanded/collapsed states
.entity-chips.chips-collapsed,
.entity-chips.chips-expanded {
position: relative;
}
// Show more/less toggle
.chips-show-more-toggle {
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;
cursor: pointer;
transition: color $es-transition-fast;
&:hover {
color: $es-primary-hover;
}
i {
font-size: 10px;
}
}
.chips-collapse-toggle,
.chips-expand-toggle {
// Specific variants inherit from .chips-show-more-toggle
}
// More chips indicator
.chips-more {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.5rem;
background: $es-slate-200;
color: $es-text-secondary;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
border-radius: $es-radius-full;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-300;
}
}
// Add chip button
.chip-add-btn {
@include button-reset;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: transparent;
color: $es-primary;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border: 1px dashed $es-primary;
border-radius: $es-radius-full;
transition: all $es-transition-fast;
&:hover {
background: $es-primary-light;
}
i {
font-size: 10px;
}
}
// Inline chips (compact mode)
.entity-chips.inline {
display: inline-flex;
padding: 0;
min-height: auto;
.entity-chip {
padding: 0.125rem 0.375rem;
font-size: 11px;
}
}
// Selected chips section in include/exclude
.selected-chips-container {
display: flex;
flex-direction: column;
gap: $es-spacing-xs;
}
.selected-chips-label {
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
color: $es-text-muted;
}
// Pattern chips (for name/reference patterns)
.entity-chip.chip-pattern {
background: #fef3c7;
color: #92400e;
font-family: monospace;
&:hover {
background: #fde68a;
}
.chip-icon {
color: #d97706;
}
}
// Range chips (price, quantity, etc.)
.entity-chip.chip-range,
.range-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: $es-cyan-50;
color: $es-cyan-600;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-full;
transition: all $es-transition-fast;
&:hover {
background: $es-cyan-100;
}
}
.range-chip-text {
font-family: monospace;
}
.btn-remove-range {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: $es-cyan-600;
border-radius: 50%;
transition: all $es-transition-fast;
&:hover {
background: rgba(0, 0, 0, 0.1);
color: $es-danger;
}
i {
font-size: 10px;
}
}
// Multi-range chips container
.multi-range-chips {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
margin-bottom: $es-spacing-xs;
&:empty {
display: none;
}
}
// Pattern chips container
.pattern-chips {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-xs;
padding: $es-spacing-sm 0;
min-height: 32px;
&:empty::before {
content: attr(data-placeholder);
color: $es-text-muted;
font-size: $es-font-size-xs;
font-style: italic;
}
}
// Pattern tag
.pattern-tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: #fef3c7;
color: #92400e;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: #fde68a;
}
&.case-sensitive {
.case-icon {
color: $es-success;
font-weight: $es-font-weight-bold;
}
}
&.draft-tag {
background: $es-white;
border: 1px solid $es-border-color;
padding: 0;
flex: 1;
min-width: 150px;
&:hover {
background: $es-white;
}
.pattern-input {
flex: 1;
min-width: 100px;
padding: 0.375rem;
border: 0;
background: transparent;
font-size: $es-font-size-sm;
font-family: inherit;
&:focus {
outline: none;
}
&::placeholder {
color: $es-text-muted;
font-style: italic;
}
}
}
}
.pattern-tag-text {
font-family: monospace;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-toggle-case {
@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(0, 0, 0, 0.1);
}
}
.case-icon {
font-size: 11px;
font-weight: $es-font-weight-semibold;
font-family: monospace;
}
.btn-remove-pattern {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: #d97706;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: rgba(0, 0, 0, 0.1);
color: $es-danger;
}
i {
font-size: 10px;
}
}
.btn-add-pattern {
@include button-reset;
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;
}
i {
font-size: 12px;
}
}
// Pattern match count (in draft tag)
.pattern-match-count {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.375rem;
color: $es-text-muted;
font-size: $es-font-size-xs;
cursor: pointer;
&.count-zero {
color: $es-warning;
}
&.count-found {
color: $es-success;
}
.count-value {
font-weight: $es-font-weight-semibold;
}
}
// Pattern input row
.pattern-input-row {
display: flex;
align-items: stretch;
gap: $es-spacing-xs;
}
}

View File

@@ -0,0 +1,395 @@
/**
* Combination Attributes Picker Component
* Product attribute combination selection styles
*/
@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: darken($es-primary-hover, 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,293 @@
/**
* 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;
}
.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;
}
// =============================================================================
// 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,362 @@
/**
* 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;
// 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 {
font-size: $es-font-size-sm;
}
.tab-label {
white-space: nowrap;
}
.tab-badge {
@include count-badge($es-primary);
}
&.has-data:not(.active) .tab-badge {
@include count-badge($es-slate-400);
}
}
// 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;
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: 12px;
}
}
}

View File

@@ -0,0 +1,842 @@
/**
* 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;
}
.exclude-rows {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.exclude-row {
display: flex;
align-items: flex-start;
gap: $es-spacing-sm;
padding: $es-spacing-sm;
background: rgba($es-danger, 0.05);
border: 1px solid rgba($es-danger, 0.2);
border-radius: $es-radius-md;
}
.exclude-row-content {
flex: 1;
}
.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: 20px;
color: $es-text-muted;
i {
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
.group-include {
margin-bottom: $es-spacing-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;
}
// Group excludes section
.group-excludes {
margin-top: $es-spacing-md;
&.has-excludes {
padding-top: $es-spacing-md;
border-top: 1px dashed $es-border-color;
}
}
.except-separator {
display: flex;
align-items: center;
margin-bottom: $es-spacing-sm;
}
.except-label {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: $es-danger-light;
color: $es-danger;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
border-radius: $es-radius-sm;
i {
font-size: 10px;
}
}
.exclude-rows-container {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.exclude-row {
padding: $es-spacing-sm;
background: rgba($es-danger, 0.03);
border: 1px solid rgba($es-danger, 0.15);
border-radius: $es-radius-md;
}
.exclude-header-row {
display: flex;
align-items: center;
gap: $es-spacing-sm;
margin-bottom: $es-spacing-sm;
}
.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: 10px;
}
}
// Group modifiers (inline version from PHP)
// Uses negative margins to break out of .group-body padding
.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: flex;
align-items: center;
gap: 0.375rem;
}
.group-modifier-limit {
width: 60px;
padding: 0.25rem 0.5rem;
font-size: $es-font-size-xs;
text-align: center;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
&:focus {
border-color: $es-primary;
outline: none;
}
}
.group-modifier-sort {
padding: 0.25rem 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: 24px;
height: 24px;
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;
}
i {
font-size: 10px;
}
}
}

View File

@@ -0,0 +1,378 @@
/**
* List Preview Component
* Entity list display in modals and popovers
*/
@use '../variables' as *;
@use '../mixins' as *;
// =============================================================================
// Preview Popover (floating popover attached to badges)
// =============================================================================
.target-preview-popover {
position: absolute;
z-index: 10000;
min-width: 280px;
max-width: 400px;
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;
}
.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-count {
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: 20px;
height: 20px;
color: $es-text-muted;
border-radius: $es-radius-sm;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-primary;
}
}
}
.preview-list {
max-height: 300px;
overflow-y: auto;
@include custom-scrollbar;
padding: $es-spacing-sm;
display: flex;
flex-direction: column;
gap: $es-spacing-xs;
}
.preview-footer {
padding: $es-spacing-sm $es-spacing-md;
background: $es-slate-50;
border-top: 1px solid $es-border-color;
text-align: center;
.preview-more-info {
font-size: $es-font-size-xs;
color: $es-text-muted;
font-weight: $es-font-weight-medium;
}
}
}
// =============================================================================
// Preview List Container
// =============================================================================
// Preview list container
.entity-list-preview {
display: flex;
flex-direction: column;
gap: $es-spacing-xs;
}
// Preview item
.preview-item {
display: flex;
align-items: center;
gap: $es-spacing-sm;
padding: $es-spacing-sm;
background: $es-white;
border: 1px solid $es-border-color;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-bg-hover;
border-color: $es-slate-300;
}
}
.preview-item-image {
flex-shrink: 0;
width: 36px;
height: 36px;
object-fit: cover;
border-radius: $es-radius-sm;
background: $es-slate-100;
}
.preview-item-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 36px;
height: 36px;
background: $es-slate-100;
color: $es-text-muted;
border-radius: $es-radius-sm;
font-size: $es-font-size-base;
}
.preview-item-info {
flex: 1;
min-width: 0;
}
.preview-item-name {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-medium;
color: $es-text-primary;
@include text-truncate;
}
.preview-item-meta {
font-size: $es-font-size-xs;
color: $es-text-muted;
@include text-truncate;
}
.preview-item-badge {
@include badge($es-slate-100, $es-text-muted);
flex-shrink: 0;
}
.preview-item-price {
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-primary;
flex-shrink: 0;
}
// Preview grid layout
.entity-list-preview.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: $es-spacing-sm;
.preview-item {
flex-direction: column;
text-align: center;
padding: $es-spacing-md;
}
.preview-item-image {
width: 60px;
height: 60px;
}
.preview-item-info {
width: 100%;
}
}
// Compact list layout
.entity-list-preview.compact {
gap: 0;
.preview-item {
border-radius: 0;
border-bottom: none;
&:first-child {
border-radius: $es-radius-md $es-radius-md 0 0;
}
&:last-child {
border-radius: 0 0 $es-radius-md $es-radius-md;
border-bottom: 1px solid $es-border-color;
}
}
}
// Preview empty state
.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 state
.preview-loading {
display: flex;
align-items: center;
justify-content: center;
padding: $es-spacing-xl;
color: $es-text-muted;
.spinner {
width: 24px;
height: 24px;
border: 3px solid $es-slate-200;
border-top-color: $es-primary;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
}
// Preview pagination
.preview-pagination {
display: flex;
align-items: center;
justify-content: center;
gap: $es-spacing-xs;
padding: $es-spacing-md 0;
}
.preview-page-btn {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 $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:not(:disabled) {
background: $es-bg-hover;
border-color: $es-slate-300;
}
&.active {
background: $es-primary;
border-color: $es-primary;
color: $es-white;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Preview summary
.preview-summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-top: 1px solid $es-border-color;
font-size: $es-font-size-xs;
color: $es-text-muted;
}
.preview-count {
font-weight: $es-font-weight-medium;
strong {
color: $es-text-primary;
}
}
// Preview filters in modal
.preview-filters {
display: flex;
flex-wrap: wrap;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-slate-50;
border-bottom: 1px solid $es-border-color;
}
.preview-filter-chip {
@include chip;
}
.preview-search {
@include input-base;
flex: 1;
min-width: 150px;
padding: 0.375rem $es-spacing-sm;
font-size: $es-font-size-xs;
}
// Scrollable preview list
.preview-list-scrollable {
max-height: 400px;
overflow-y: auto;
@include custom-scrollbar;
}
// Mini preview (inline)
.entity-mini-preview {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: $es-slate-50;
border-radius: $es-radius-sm;
font-size: $es-font-size-xs;
img {
width: 16px;
height: 16px;
object-fit: cover;
border-radius: 2px;
}
.mini-preview-name {
color: $es-text-primary;
font-weight: $es-font-weight-medium;
@include text-truncate;
max-width: 100px;
}
}

View File

@@ -0,0 +1,214 @@
/**
* 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;
opacity: 0;
pointer-events: none;
width: 0;
height: 0;
}
}
// =============================================================================
// 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,275 @@
/**
* Modal Component
* Preview modals, confirmation dialogs
*/
@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: darken($es-danger, 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;
}
}

View File

@@ -0,0 +1,308 @@
/**
* 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;
}
}

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,178 @@
/**
* Tooltip Component
* Info tooltips and help popovers
*/
@use '../variables' as *;
@use '../mixins' as *;
// =============================================================================
// MPR Info Wrapper (hover tooltip trigger)
// =============================================================================
.mpr-info-wrapper {
display: inline-flex;
align-items: center;
position: relative;
cursor: help;
vertical-align: middle;
margin-left: 0.5rem;
}
// Tooltip (absolute positioned, follows element)
.mpr-info-wrapper .mpr-tooltip {
position: absolute;
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.625;
white-space: normal;
z-index: 1050;
max-width: 350px;
min-width: 200px;
text-align: left;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
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;
// Arrow (border)
&::before {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 9px solid transparent;
border-top-color: rgba(64, 68, 82, 0.16);
}
// Arrow (fill)
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 8px solid transparent;
border-top-color: $es-white;
}
strong {
display: block;
margin-bottom: 0.5rem;
font-weight: $es-font-weight-semibold;
color: #337ab7;
}
p {
margin: 0;
color: $es-text-secondary;
}
}
// =============================================================================
// Fixed Tooltip (appended to body)
// =============================================================================
.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.625;
white-space: normal;
z-index: 10500;
max-width: 350px;
min-width: 200px;
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;
strong {
display: block;
margin-bottom: 0.5rem;
font-weight: $es-font-weight-semibold;
color: #337ab7;
}
p {
margin: 0;
color: $es-text-secondary;
}
}
// =============================================================================
// MPR Icon (SVG mask icons)
// =============================================================================
.mpr-icon {
display: inline-block;
width: 16px;
height: 16px;
background-color: $es-slate-600;
vertical-align: middle;
cursor: pointer;
transition: background-color 0.2s;
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
&:hover {
background-color: #5bc0de;
}
&.link {
background-color: #5bc0de;
&:hover {
background-color: #337ab7;
}
}
}
// Info icon
.mpr-icon.icon-info {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 2.5H5A2.5 2.5 0 0 0 2.5 5v6A2.5 2.5 0 0 0 5 13.5h6a2.5 2.5 0 0 0 2.5-2.5V5A2.5 2.5 0 0 0 11 2.5ZM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5Z' fill='%23414552'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.25 8A.75.75 0 0 1 7 7.25h1.25A.75.75 0 0 1 9 8v3.5a.75.75 0 0 1-1.5 0V8.75H7A.75.75 0 0 1 6.25 8Z' fill='%23414552'/%3E%3Cpath d='M6.75 5a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0Z' fill='%23414552'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 2.5H5A2.5 2.5 0 0 0 2.5 5v6A2.5 2.5 0 0 0 5 13.5h6a2.5 2.5 0 0 0 2.5-2.5V5A2.5 2.5 0 0 0 11 2.5ZM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5Z' fill='%23414552'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.25 8A.75.75 0 0 1 7 7.25h1.25A.75.75 0 0 1 9 8v3.5a.75.75 0 0 1-1.5 0V8.75H7A.75.75 0 0 1 6.25 8Z' fill='%23414552'/%3E%3Cpath d='M6.75 5a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0Z' fill='%23414552'/%3E%3C/svg%3E");
}
// =============================================================================
// Tooltip Content Styling
// =============================================================================
.tooltip-list {
margin: 0.5rem 0;
> div {
margin: 0.25rem 0;
padding-left: 0.5rem;
}
}
.tooltip-example {
font-family: monospace;
font-size: 12px;
background: $es-slate-100;
padding: 0.25rem 0.5rem;
border-radius: $es-radius-sm;
margin: 0.25rem 0;
}
.tooltip-logic {
font-size: 11px;
color: $es-text-muted;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid $es-border-color;
}

View File

@@ -0,0 +1,242 @@
/**
* 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);
}
}
.entity-search-icon {
color: $es-text-muted;
font-size: 14px;
flex-shrink: 0;
}
// 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;
}
}
// 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.625rem;
color: $es-text-secondary;
background: $es-slate-100;
border: 1px solid transparent;
border-radius: $es-radius-sm;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
cursor: pointer;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
}
&.selected {
color: $es-primary;
background: $es-primary-light;
border-color: $es-primary;
}
i {
font-size: 12px;
}
}
.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;

1
sources/scss/index.php Normal file
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,81 @@
/**
* 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()
.form-group:has(.entity-selector-trait),
.form-group:has(.target-conditions-trait),
.form-group:has(.condition-trait) {
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;
}
}
// 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;
}

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;

30
sources/scss/main.scss Normal file
View File

@@ -0,0 +1,30 @@
/**
* 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/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';

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 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

View File

@@ -789,7 +789,7 @@ trait ScheduleConditions
$jsConfigJson = htmlspecialchars(json_encode($jsConfig), ENT_QUOTES, 'UTF-8'); $jsConfigJson = htmlspecialchars(json_encode($jsConfig), ENT_QUOTES, 'UTF-8');
$html = '<div class="target-conditions-trait holiday-countries-target"'; $html = '<div class="target-conditions-trait holiday-countries-target"';
$html .= ' data-target-conditions-id="holiday-countries-target"'; $html .= ' data-entity-selector-id="holiday-countries-target"';
$html .= ' data-mode="multi"'; $html .= ' data-mode="multi"';
$html .= ' data-config=\'' . $jsConfigJson . '\'>'; $html .= ' data-config=\'' . $jsConfigJson . '\'>';