Add selection validation and improve tooltip component
- Add validation system to prevent contradicting conditions: - Same entity in include/exclude detection - Parent-child conflict detection for tree entities - Redundant selection prevention - Toast notifications for validation errors - Fix entity icons (employees: briefcase, taxes: calculator) - Improve tooltip component: - Use Material Icons instead of broken FA4 icons - Fix positioning using getBoundingClientRect for viewport coords - Add click-to-pin functionality with close button - Pinned tooltips show X icon and close button in corner - Add lightweight test suite (31 tests) for validation logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1174,6 +1174,14 @@
|
||||
$item.toggleClass('selected');
|
||||
self.serializeAllBlocks($row);
|
||||
} else {
|
||||
// Validate selection before adding
|
||||
var section = self.activeGroup.section;
|
||||
var validation = self.validateSelection(id, name, section, $item.data());
|
||||
if (!validation.valid) {
|
||||
self.showValidationError(validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentSelection = self.getCurrentSingleSelection();
|
||||
if (currentSelection) {
|
||||
var newEntityType = self.activeGroup.blockType;
|
||||
@@ -1225,6 +1233,9 @@
|
||||
e.preventDefault();
|
||||
if (!self.activeGroup) return;
|
||||
|
||||
var section = self.activeGroup.section;
|
||||
var skippedCount = 0;
|
||||
|
||||
// Handle tree view - use pending selections
|
||||
if (self.viewMode === 'tree') {
|
||||
if (!self.pendingSelections) self.pendingSelections = [];
|
||||
@@ -1237,6 +1248,13 @@
|
||||
var name = $item.data('name');
|
||||
|
||||
if (!$item.hasClass('selected')) {
|
||||
// Validate before adding
|
||||
var validation = self.validateSelection(id, name, section, $item.data());
|
||||
if (!validation.valid) {
|
||||
skippedCount++;
|
||||
return; // Skip this item
|
||||
}
|
||||
|
||||
$item.addClass('selected');
|
||||
var exists = self.pendingSelections.some(function(s) {
|
||||
return parseInt(s.id, 10) === id;
|
||||
@@ -1247,6 +1265,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Show warning if items were skipped
|
||||
if (skippedCount > 0) {
|
||||
var trans = self.config.trans || {};
|
||||
var msg = (trans.items_skipped_conflicts || '{count} item(s) were skipped due to conflicts.').replace('{count}', skippedCount);
|
||||
self.showValidationError(msg);
|
||||
}
|
||||
|
||||
// Update count display
|
||||
var selectedCount = self.$dropdown.find('.tree-item.selected').length;
|
||||
var totalCount = self.$dropdown.find('.tree-item').length;
|
||||
@@ -1276,11 +1301,26 @@
|
||||
if (!$(this).hasClass('selected')) {
|
||||
var id = $(this).data('id');
|
||||
var name = $(this).data('name');
|
||||
|
||||
// Validate before adding
|
||||
var validation = self.validateSelection(id, name, section, $(this).data());
|
||||
if (!validation.valid) {
|
||||
skippedCount++;
|
||||
return; // Skip this item
|
||||
}
|
||||
|
||||
self.addSelectionNoUpdate($picker, id, name, $(this).data());
|
||||
$(this).addClass('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// Show warning if items were skipped
|
||||
if (skippedCount > 0) {
|
||||
var trans = self.config.trans || {};
|
||||
var msg = (trans.items_skipped_conflicts || '{count} item(s) were skipped due to conflicts.').replace('{count}', skippedCount);
|
||||
self.showValidationError(msg);
|
||||
}
|
||||
|
||||
var $chips = $picker.find('.entity-chips');
|
||||
self.updateChipsVisibility($chips);
|
||||
|
||||
@@ -1448,6 +1488,14 @@
|
||||
});
|
||||
$item.removeClass('selected');
|
||||
} else {
|
||||
// Validate selection before adding to pending
|
||||
var section = self.activeGroup.section;
|
||||
var validation = self.validateSelection(id, name, section, $item.data());
|
||||
if (!validation.valid) {
|
||||
self.showValidationError(validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to pending
|
||||
var exists = self.pendingSelections.some(function(s) {
|
||||
return parseInt(s.id, 10) === id;
|
||||
@@ -1529,19 +1577,41 @@
|
||||
$btn.find('i').removeClass('icon-minus-square').addClass('icon-plus-square');
|
||||
$btn.attr('title', trans.select_with_children || 'Select with all children');
|
||||
} else {
|
||||
var section = self.activeGroup.section;
|
||||
var skippedChildren = 0;
|
||||
|
||||
if (!$item.hasClass('selected')) {
|
||||
self.addSelectionNoUpdate($picker, $item.data('id'), $item.data('name'), $item.data());
|
||||
$item.addClass('selected');
|
||||
// Validate parent first
|
||||
var parentValidation = self.validateSelection($item.data('id'), $item.data('name'), section, $item.data());
|
||||
if (parentValidation.valid) {
|
||||
self.addSelectionNoUpdate($picker, $item.data('id'), $item.data('name'), $item.data());
|
||||
$item.addClass('selected');
|
||||
} else {
|
||||
self.showValidationError(parentValidation.error);
|
||||
return; // Can't select parent, abort entire operation
|
||||
}
|
||||
}
|
||||
|
||||
for (var k = 0; k < descendants.length; k++) {
|
||||
var $descendant = $(descendants[k]);
|
||||
if (!$descendant.hasClass('selected')) {
|
||||
self.addSelectionNoUpdate($picker, $descendant.data('id'), $descendant.data('name'), $descendant.data());
|
||||
$descendant.addClass('selected');
|
||||
// Validate each child
|
||||
var childValidation = self.validateSelection($descendant.data('id'), $descendant.data('name'), section, $descendant.data());
|
||||
if (childValidation.valid) {
|
||||
self.addSelectionNoUpdate($picker, $descendant.data('id'), $descendant.data('name'), $descendant.data());
|
||||
$descendant.addClass('selected');
|
||||
} else {
|
||||
skippedChildren++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show warning if children were skipped
|
||||
if (skippedChildren > 0) {
|
||||
var skipMsg = (trans.children_skipped_conflicts || '{count} child item(s) were skipped due to conflicts.').replace('{count}', skippedChildren);
|
||||
self.showValidationError(skipMsg);
|
||||
}
|
||||
|
||||
$btn.find('i').removeClass('icon-plus-square').addClass('icon-minus-square');
|
||||
$btn.attr('title', trans.deselect_with_children || 'Deselect with all children');
|
||||
}
|
||||
@@ -1958,6 +2028,105 @@
|
||||
self.hideDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Tooltip hover events
|
||||
this.$wrapper.on('mouseenter', '.mpr-info-wrapper:not(.pinned)', function() {
|
||||
var $wrapper = $(this);
|
||||
var content = $wrapper.attr('data-tooltip');
|
||||
if (!content) return;
|
||||
|
||||
// Don't show hover tooltip if another is pinned
|
||||
if ($('.mpr-tooltip-fixed.pinned').length) return;
|
||||
|
||||
// Remove any existing non-pinned tooltip
|
||||
$('.mpr-tooltip-fixed:not(.pinned)').remove();
|
||||
|
||||
// Create tooltip
|
||||
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed' }).html(content);
|
||||
$('body').append($tooltip);
|
||||
|
||||
// Use getBoundingClientRect for viewport-relative positioning (fixed)
|
||||
var rect = $wrapper[0].getBoundingClientRect();
|
||||
var tooltipWidth = $tooltip.outerWidth();
|
||||
var tooltipHeight = $tooltip.outerHeight();
|
||||
|
||||
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
|
||||
var top = rect.top - tooltipHeight - 8;
|
||||
|
||||
// Keep tooltip within viewport
|
||||
if (left < 10) left = 10;
|
||||
if (left + tooltipWidth > window.innerWidth - 10) {
|
||||
left = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (top < 10) {
|
||||
top = rect.bottom + 8;
|
||||
}
|
||||
|
||||
$tooltip.css({ top: top, left: left });
|
||||
});
|
||||
|
||||
this.$wrapper.on('mouseleave', '.mpr-info-wrapper:not(.pinned)', function() {
|
||||
$('.mpr-tooltip-fixed:not(.pinned)').remove();
|
||||
});
|
||||
|
||||
// Click to pin tooltip
|
||||
this.$wrapper.on('click', '.mpr-info-wrapper', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var $wrapper = $(this);
|
||||
|
||||
// If already pinned, unpin and close
|
||||
if ($wrapper.hasClass('pinned')) {
|
||||
$wrapper.removeClass('pinned');
|
||||
$wrapper.find('.material-icons').text('info_outline');
|
||||
$('.mpr-tooltip-fixed.pinned').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any other pinned tooltips
|
||||
$('.mpr-info-wrapper.pinned').removeClass('pinned').find('.material-icons').text('info_outline');
|
||||
$('.mpr-tooltip-fixed').remove();
|
||||
|
||||
var content = $wrapper.attr('data-tooltip');
|
||||
if (!content) return;
|
||||
|
||||
// Pin this one
|
||||
$wrapper.addClass('pinned');
|
||||
$wrapper.find('.material-icons').text('close');
|
||||
|
||||
// Create pinned tooltip with close button
|
||||
var $tooltip = $('<div>', { class: 'mpr-tooltip-fixed pinned' });
|
||||
var $closeBtn = $('<button>', { class: 'mpr-tooltip-close', type: 'button' })
|
||||
.append($('<i>', { class: 'material-icons', text: 'close' }));
|
||||
$tooltip.append($closeBtn).append(content);
|
||||
$('body').append($tooltip);
|
||||
|
||||
// Close button click
|
||||
$closeBtn.on('click', function() {
|
||||
$wrapper.removeClass('pinned');
|
||||
$wrapper.find('.material-icons').text('info_outline');
|
||||
$tooltip.remove();
|
||||
});
|
||||
|
||||
// Position
|
||||
var rect = $wrapper[0].getBoundingClientRect();
|
||||
var tooltipWidth = $tooltip.outerWidth();
|
||||
var tooltipHeight = $tooltip.outerHeight();
|
||||
|
||||
var left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
|
||||
var top = rect.top - tooltipHeight - 8;
|
||||
|
||||
if (left < 10) left = 10;
|
||||
if (left + tooltipWidth > window.innerWidth - 10) {
|
||||
left = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (top < 10) {
|
||||
top = rect.bottom + 8;
|
||||
}
|
||||
|
||||
$tooltip.css({ top: top, left: left });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6987,9 +7156,9 @@
|
||||
if (helpContent) {
|
||||
var $infoWrapper = $('<span>', {
|
||||
class: 'mpr-info-wrapper',
|
||||
'data-details': helpContent
|
||||
'data-tooltip': helpContent
|
||||
});
|
||||
$infoWrapper.append($('<span>', { class: 'mpr-icon icon-info link' }));
|
||||
$infoWrapper.append($('<i>', { class: 'material-icons', text: 'info_outline' }));
|
||||
$placeholder.append($infoWrapper);
|
||||
}
|
||||
},
|
||||
@@ -9099,6 +9268,372 @@
|
||||
|
||||
})(jQuery);
|
||||
|
||||
/**
|
||||
* Entity Selector - Validation Module
|
||||
* Conflict detection and prevention for entity selections
|
||||
* @partial _validation.js
|
||||
*
|
||||
* Features:
|
||||
* - Same entity in include & exclude detection
|
||||
* - Parent-child conflict detection for tree entities
|
||||
* - Redundant selection detection
|
||||
* - Error message display
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
window._EntitySelectorMixins = window._EntitySelectorMixins || {};
|
||||
|
||||
window._EntitySelectorMixins.validation = {
|
||||
|
||||
/**
|
||||
* Validate a selection before adding it
|
||||
* Returns { valid: true } or { valid: false, error: 'message', type: 'conflict_type' }
|
||||
*
|
||||
* @param {number|string} id - Entity ID to validate
|
||||
* @param {string} name - Entity name (for error messages)
|
||||
* @param {string} section - 'include' or 'exclude'
|
||||
* @param {Object} data - Additional data (parent_id, etc.)
|
||||
* @returns {Object} Validation result
|
||||
*/
|
||||
validateSelection: function(id, name, section, data) {
|
||||
if (!this.activeGroup) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
var trans = this.config.trans || {};
|
||||
id = parseInt(id, 10);
|
||||
|
||||
var $block = this.$wrapper.find('.target-block[data-block-type="' + this.activeGroup.blockType + '"]');
|
||||
var $group = $block.find('.selection-group[data-group-index="' + this.activeGroup.groupIndex + '"]');
|
||||
|
||||
// Get include chips
|
||||
var includeIds = this.getChipIds($group.find('.include-picker'));
|
||||
|
||||
// Get all exclude chips (from all exclude rows)
|
||||
var excludeIds = [];
|
||||
$group.find('.exclude-row').each(function() {
|
||||
var $excludePicker = $(this).find('.exclude-picker');
|
||||
var ids = [];
|
||||
$excludePicker.find('.entity-chip').each(function() {
|
||||
ids.push(parseInt($(this).data('id'), 10));
|
||||
});
|
||||
excludeIds = excludeIds.concat(ids);
|
||||
});
|
||||
|
||||
// 1. Check for same entity in include & exclude
|
||||
var conflictResult = this.checkIncludeExcludeConflict(id, name, section, includeIds, excludeIds, trans);
|
||||
if (!conflictResult.valid) {
|
||||
return conflictResult;
|
||||
}
|
||||
|
||||
// 2. Check for redundant selection (already selected in same section)
|
||||
var redundantResult = this.checkRedundantSelection(id, name, section, includeIds, excludeIds, trans);
|
||||
if (!redundantResult.valid) {
|
||||
return redundantResult;
|
||||
}
|
||||
|
||||
// 3. Check for parent-child conflicts (only for tree entities)
|
||||
var searchEntity = this.activeGroup.searchEntity;
|
||||
if (searchEntity === 'categories' || searchEntity === 'cms_categories') {
|
||||
var treeResult = this.checkTreeConflicts(id, name, section, data, includeIds, excludeIds, trans);
|
||||
if (!treeResult.valid) {
|
||||
return treeResult;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if entity is in both include and exclude
|
||||
*/
|
||||
checkIncludeExcludeConflict: function(id, name, section, includeIds, excludeIds, trans) {
|
||||
if (section === 'include' && excludeIds.indexOf(id) !== -1) {
|
||||
return {
|
||||
valid: false,
|
||||
type: 'include_exclude_conflict',
|
||||
error: (trans.error_in_exclude || '"{name}" is already in the exclude list. Remove it from exclude first.').replace('{name}', name)
|
||||
};
|
||||
}
|
||||
|
||||
if (section === 'exclude' && includeIds.indexOf(id) !== -1) {
|
||||
return {
|
||||
valid: false,
|
||||
type: 'include_exclude_conflict',
|
||||
error: (trans.error_in_include || '"{name}" is already in the include list. Remove it from include first.').replace('{name}', name)
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Check for redundant selection (already selected)
|
||||
*/
|
||||
checkRedundantSelection: function(id, name, section, includeIds, excludeIds, trans) {
|
||||
if (section === 'include' && includeIds.indexOf(id) !== -1) {
|
||||
return {
|
||||
valid: false,
|
||||
type: 'redundant',
|
||||
error: (trans.error_already_selected || '"{name}" is already selected.').replace('{name}', name)
|
||||
};
|
||||
}
|
||||
|
||||
if (section === 'exclude' && excludeIds.indexOf(id) !== -1) {
|
||||
return {
|
||||
valid: false,
|
||||
type: 'redundant',
|
||||
error: (trans.error_already_excluded || '"{name}" is already in an exclude list.').replace('{name}', name)
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Check for parent-child conflicts in tree entities
|
||||
*/
|
||||
checkTreeConflicts: function(id, name, section, data, includeIds, excludeIds, trans) {
|
||||
// Need tree data for parent-child lookups
|
||||
if (!this.treeFlatData) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
var parentId = data && data.parentId ? parseInt(data.parentId, 10) : null;
|
||||
|
||||
// Build lookup for quick access
|
||||
var lookup = {};
|
||||
this.treeFlatData.forEach(function(item) {
|
||||
lookup[parseInt(item.id, 10)] = item;
|
||||
});
|
||||
|
||||
// Get all ancestor IDs
|
||||
var ancestorIds = this.getAncestorIds(id, lookup);
|
||||
|
||||
// Get all descendant IDs
|
||||
var descendantIds = this.getDescendantIds(id, lookup);
|
||||
|
||||
if (section === 'include') {
|
||||
// Check if any ancestor is excluded
|
||||
for (var i = 0; i < ancestorIds.length; i++) {
|
||||
if (excludeIds.indexOf(ancestorIds[i]) !== -1) {
|
||||
var ancestorName = lookup[ancestorIds[i]] ? lookup[ancestorIds[i]].name : 'Parent';
|
||||
return {
|
||||
valid: false,
|
||||
type: 'parent_excluded',
|
||||
error: (trans.error_parent_excluded || 'Cannot include "{name}" because its parent "{parent}" is excluded.').replace('{name}', name).replace('{parent}', ancestorName)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any descendant is excluded
|
||||
for (var j = 0; j < descendantIds.length; j++) {
|
||||
if (excludeIds.indexOf(descendantIds[j]) !== -1) {
|
||||
var descendantName = lookup[descendantIds[j]] ? lookup[descendantIds[j]].name : 'Child';
|
||||
return {
|
||||
valid: false,
|
||||
type: 'child_excluded',
|
||||
error: (trans.error_child_excluded || 'Cannot include "{name}" because its child "{child}" is excluded. Remove the child from exclude first.').replace('{name}', name).replace('{child}', descendantName)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (section === 'exclude') {
|
||||
// Check if any ancestor is included
|
||||
for (var k = 0; k < ancestorIds.length; k++) {
|
||||
if (includeIds.indexOf(ancestorIds[k]) !== -1) {
|
||||
var parentName = lookup[ancestorIds[k]] ? lookup[ancestorIds[k]].name : 'Parent';
|
||||
return {
|
||||
valid: false,
|
||||
type: 'parent_included',
|
||||
error: (trans.error_parent_included || 'Cannot exclude "{name}" because its parent "{parent}" is included. This would create a contradiction.').replace('{name}', name).replace('{parent}', parentName)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any descendant is included (warning about implicit exclusion)
|
||||
var includedDescendants = [];
|
||||
for (var m = 0; m < descendantIds.length; m++) {
|
||||
if (includeIds.indexOf(descendantIds[m]) !== -1) {
|
||||
var childName = lookup[descendantIds[m]] ? lookup[descendantIds[m]].name : 'Child';
|
||||
includedDescendants.push(childName);
|
||||
}
|
||||
}
|
||||
|
||||
if (includedDescendants.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
type: 'children_included',
|
||||
error: (trans.error_children_included || 'Cannot exclude "{name}" because its children ({children}) are included. Remove them from include first.').replace('{name}', name).replace('{children}', includedDescendants.slice(0, 3).join(', ') + (includedDescendants.length > 3 ? '...' : ''))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all ancestor IDs for a given item
|
||||
*/
|
||||
getAncestorIds: function(id, lookup) {
|
||||
var ancestors = [];
|
||||
var current = lookup[id];
|
||||
|
||||
while (current && current.parent_id) {
|
||||
var parentId = parseInt(current.parent_id, 10);
|
||||
if (parentId && lookup[parentId]) {
|
||||
ancestors.push(parentId);
|
||||
current = lookup[parentId];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all descendant IDs for a given item
|
||||
*/
|
||||
getDescendantIds: function(id, lookup) {
|
||||
var descendants = [];
|
||||
var self = this;
|
||||
|
||||
// Find direct children
|
||||
Object.keys(lookup).forEach(function(key) {
|
||||
var item = lookup[key];
|
||||
if (parseInt(item.parent_id, 10) === id) {
|
||||
var childId = parseInt(item.id, 10);
|
||||
descendants.push(childId);
|
||||
// Recursively get children's descendants
|
||||
var childDescendants = self.getDescendantIds(childId, lookup);
|
||||
descendants = descendants.concat(childDescendants);
|
||||
}
|
||||
});
|
||||
|
||||
return descendants;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get chip IDs from a picker
|
||||
*/
|
||||
getChipIds: function($picker) {
|
||||
var ids = [];
|
||||
$picker.find('.entity-chip').each(function() {
|
||||
ids.push(parseInt($(this).data('id'), 10));
|
||||
});
|
||||
return ids;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate pending selections (for tree view bulk operations)
|
||||
* Returns array of invalid items
|
||||
*/
|
||||
validatePendingSelections: function(pendingSelections, section) {
|
||||
var self = this;
|
||||
var errors = [];
|
||||
|
||||
if (!pendingSelections || !pendingSelections.length) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
pendingSelections.forEach(function(sel) {
|
||||
var result = self.validateSelection(sel.id, sel.name, section, sel.data || {});
|
||||
if (!result.valid) {
|
||||
errors.push({
|
||||
id: sel.id,
|
||||
name: sel.name,
|
||||
error: result.error,
|
||||
type: result.type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show validation error toast
|
||||
*/
|
||||
showValidationError: function(message) {
|
||||
var trans = this.config.trans || {};
|
||||
var title = trans.validation_error || 'Selection Conflict';
|
||||
|
||||
// Remove existing toast
|
||||
$('.es-validation-toast').remove();
|
||||
|
||||
// Create toast HTML
|
||||
var html = '<div class="es-validation-toast">';
|
||||
html += '<div class="es-toast-icon"><i class="icon-exclamation-triangle"></i></div>';
|
||||
html += '<div class="es-toast-content">';
|
||||
html += '<div class="es-toast-title">' + this.escapeHtml(title) + '</div>';
|
||||
html += '<div class="es-toast-message">' + this.escapeHtml(message) + '</div>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" class="es-toast-close"><i class="icon-times"></i></button>';
|
||||
html += '</div>';
|
||||
|
||||
var $toast = $(html);
|
||||
$('body').append($toast);
|
||||
|
||||
// Position near dropdown if visible
|
||||
if (this.$dropdown && this.$dropdown.hasClass('show')) {
|
||||
var dropdownOffset = this.$dropdown.offset();
|
||||
$toast.css({
|
||||
position: 'fixed',
|
||||
top: dropdownOffset.top - $toast.outerHeight() - 10,
|
||||
left: dropdownOffset.left,
|
||||
zIndex: 10001
|
||||
});
|
||||
} else {
|
||||
$toast.css({
|
||||
position: 'fixed',
|
||||
top: 20,
|
||||
right: 20,
|
||||
zIndex: 10001
|
||||
});
|
||||
}
|
||||
|
||||
// Animate in
|
||||
$toast.hide().fadeIn(200);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$toast.fadeOut(200, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Close button
|
||||
$toast.on('click', '.es-toast-close', function() {
|
||||
$toast.fadeOut(200, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate and add selection (wrapper that validates before adding)
|
||||
* Returns true if added successfully, false if validation failed
|
||||
*/
|
||||
validateAndAddSelection: function($picker, id, name, data, section) {
|
||||
var result = this.validateSelection(id, name, section, data || {});
|
||||
|
||||
if (!result.valid) {
|
||||
this.showValidationError(result.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validation passed, add the selection
|
||||
this.addSelection($picker, id, name, data);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
/**
|
||||
* Entity Selector - Core Module
|
||||
* Factory, initialization, state management
|
||||
|
||||
2
assets/js/admin/entity-selector.min.js
vendored
2
assets/js/admin/entity-selector.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user