diff --git a/iframe.html b/iframe.html
index bd2d3e1..9f2010a 100644
--- a/iframe.html
+++ b/iframe.html
@@ -1657,7 +1657,7 @@
this.redistributePercentages();
this.renderFoodSource(foodSource);
this.updateAddButton();
- this.updateFoodCalculations();
+ this.refreshAllPercentageUI();
}
removeFoodSource(id) {
@@ -1680,7 +1680,7 @@
this.redistributePercentages();
this.updateFoodSourceNames();
this.updateAddButton();
- this.updateFoodCalculations();
+ this.refreshAllPercentageUI();
}
generateFoodSourceId() {
@@ -1711,9 +1711,13 @@
}
// Update the UI sliders and inputs
- this.updatePercentageInputs();
+ this.refreshAllPercentageUI();
}
+ // OBSOLETE METHODS - Replaced by new validation system
+ // Keeping for reference but these are no longer used
+
+ /*
updatePercentageInputs() {
this.foodSources.forEach(fs => {
const slider = document.getElementById(`percentage-slider-${fs.id}`);
@@ -1836,6 +1840,187 @@
this.updatePercentageInputs();
this.updateFoodCalculations();
}
+ */
+
+ // New validation system methods
+ validatePercentageChange(sourceId, requestedValue) {
+ // Find the source being changed
+ const changedSource = this.foodSources.find(fs => fs.id === sourceId);
+ if (!changedSource) {
+ return { isValid: false, reason: 'Source not found' };
+ }
+
+ // If the source is locked, no change allowed
+ if (changedSource.isLocked) {
+ return { isValid: false, reason: 'Source is locked' };
+ }
+
+ // Ensure requested value is within bounds
+ const clampedValue = Math.max(0, Math.min(100, requestedValue));
+
+ // Calculate locked and other unlocked totals
+ const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked);
+ const otherUnlockedSources = this.foodSources.filter(fs => fs.id !== sourceId && !fs.isLocked);
+
+ const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
+
+ // Check if the only unlocked source
+ if (otherUnlockedSources.length === 0) {
+ // This is the only unlocked source, must fill remaining percentage
+ const requiredPercentage = 100 - totalLocked;
+ return {
+ isValid: true,
+ actualValue: requiredPercentage,
+ affectedSources: [{ id: sourceId, newPercentage: requiredPercentage }],
+ reason: 'Only unlocked source, forced to fill remainder'
+ };
+ }
+
+ // Calculate available percentage for redistribution
+ const availableForOthers = 100 - clampedValue - totalLocked;
+
+ // Check if redistribution is possible
+ if (availableForOthers < 0) {
+ // Cannot accommodate this value
+ const maxAllowed = 100 - totalLocked;
+ return {
+ isValid: true,
+ actualValue: maxAllowed,
+ affectedSources: this.calculateRedistribution(sourceId, maxAllowed, otherUnlockedSources),
+ reason: 'Value clamped to maximum allowed'
+ };
+ }
+
+ // Calculate redistribution
+ const affectedSources = this.calculateRedistribution(sourceId, clampedValue, otherUnlockedSources);
+
+ return {
+ isValid: true,
+ actualValue: clampedValue,
+ affectedSources: affectedSources,
+ reason: 'Valid change'
+ };
+ }
+
+ calculateRedistribution(sourceId, newValue, otherUnlockedSources) {
+ const result = [{ id: sourceId, newPercentage: newValue }];
+
+ if (otherUnlockedSources.length === 0) {
+ return result;
+ }
+
+ // Calculate total locked percentage
+ const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked);
+ const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
+
+ // Available percentage for other unlocked sources
+ const availableForOthers = 100 - newValue - totalLocked;
+
+ // Current total of other unlocked sources
+ const currentOtherTotal = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
+
+ if (currentOtherTotal === 0 || availableForOthers === 0) {
+ // Distribute equally among other unlocked sources
+ const equalShare = Math.floor(availableForOthers / otherUnlockedSources.length);
+ const remainder = availableForOthers - (equalShare * otherUnlockedSources.length);
+
+ otherUnlockedSources.forEach((fs, index) => {
+ const newPercentage = equalShare + (index < remainder ? 1 : 0);
+ result.push({ id: fs.id, newPercentage });
+ });
+ } else {
+ // Distribute proportionally
+ const scale = availableForOthers / currentOtherTotal;
+ let distributedTotal = 0;
+
+ otherUnlockedSources.forEach((fs, index) => {
+ let newPercentage;
+ if (index === otherUnlockedSources.length - 1) {
+ // Last item gets remainder to ensure exact total
+ newPercentage = availableForOthers - distributedTotal;
+ } else {
+ newPercentage = Math.round(fs.percentage * scale);
+ distributedTotal += newPercentage;
+ }
+ result.push({ id: fs.id, newPercentage });
+ });
+ }
+
+ return result;
+ }
+
+ applyValidatedChanges(validationResult) {
+ if (!validationResult.isValid) {
+ return false;
+ }
+
+ // Apply all percentage changes
+ validationResult.affectedSources.forEach(change => {
+ const source = this.foodSources.find(fs => fs.id === change.id);
+ if (source) {
+ source.percentage = change.newPercentage;
+ }
+ });
+
+ return true;
+ }
+
+ refreshAllPercentageUI() {
+ this.foodSources.forEach(fs => {
+ // Update all UI elements from single source of truth
+ const slider = document.getElementById(`percentage-slider-${fs.id}`);
+ const input = document.getElementById(`percentage-input-${fs.id}`);
+ const display = document.getElementById(`percentage-display-${fs.id}`);
+
+ if (slider) slider.value = fs.percentage;
+ if (input) input.value = fs.percentage;
+ if (display) display.textContent = `${fs.percentage}%`;
+
+ // Update constraints and disabled states
+ this.updateSliderConstraints(fs);
+ });
+
+ // Update food calculations
+ this.updateFoodCalculations();
+ }
+
+ updateSliderConstraints(foodSource) {
+ const slider = document.getElementById(`percentage-slider-${foodSource.id}`);
+ const input = document.getElementById(`percentage-input-${foodSource.id}`);
+
+ if (!slider || !input) return;
+
+ // Always keep 0-100 scale
+ slider.max = 100;
+ input.max = 100;
+
+ if (foodSource.isLocked) {
+ slider.disabled = true;
+ input.disabled = true;
+ } else {
+ // Calculate maximum allowed and store for validation
+ const maxAllowed = this.calculateMaxAllowed(foodSource.id);
+ slider.disabled = (maxAllowed <= 0);
+ input.disabled = (maxAllowed <= 0);
+ slider.dataset.maxAllowed = maxAllowed;
+ input.dataset.maxAllowed = maxAllowed;
+ }
+ }
+
+ calculateMaxAllowed(sourceId) {
+ const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked);
+ const otherUnlockedSources = this.foodSources.filter(fs => fs.id !== sourceId && !fs.isLocked);
+
+ const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
+
+ // If this is the only unlocked source, it must take up the remainder
+ if (otherUnlockedSources.length === 0) {
+ return 100 - totalLocked;
+ }
+
+ // Otherwise, maximum is 100 minus locked percentages
+ return Math.max(0, 100 - totalLocked);
+ }
updateFoodSourceNames() {
this.foodSources.forEach((fs, index) => {
@@ -1940,65 +2125,26 @@
if (percentageSlider) {
percentageSlider.addEventListener('input', () => {
- // Check if this source is locked first
- const foodSource = this.foodSources.find(fs => fs.id === id);
- if (foodSource && foodSource.isLocked) {
- // Reset slider to original value and return
- percentageSlider.value = foodSource.percentage;
- return;
+ const requestedValue = parseInt(percentageSlider.value);
+ const result = this.validatePercentageChange(id, requestedValue);
+
+ if (result.isValid) {
+ this.applyValidatedChanges(result);
}
-
- // Check if this source has no flexibility (either only unlocked or no percentage available)
- const otherUnlockedSources = this.foodSources.filter((fs, index) =>
- fs.id !== id && !fs.isLocked
- );
-
- // Calculate available percentage for this source
- const lockedSources = this.foodSources.filter(fs => fs.id !== id && fs.isLocked);
- const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
- const otherUnlockedPercentage = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
- const availablePercentage = 100 - totalLockedPercentage - otherUnlockedPercentage;
-
- if (otherUnlockedSources.length === 0 || availablePercentage <= 0) {
- // This source has no flexibility - don't update display, just call adjustPercentages
- // which will force it to the correct value and update display properly
- this.adjustPercentages(id, parseInt(percentageSlider.value));
- return;
- }
-
- let newPercentage = parseInt(percentageSlider.value);
- const maxAllowed = parseInt(percentageSlider.dataset.maxAllowed) || 100;
-
- // Constrain to max allowed but keep slider scale 0-100
- if (newPercentage > maxAllowed) {
- newPercentage = maxAllowed;
- percentageSlider.value = maxAllowed;
- }
-
- this.adjustPercentages(id, newPercentage);
- document.getElementById(`percentage-display-${id}`).textContent = `${newPercentage}%`;
+ // Always refresh to ensure valid state
+ this.refreshAllPercentageUI();
});
}
if (percentageInput) {
percentageInput.addEventListener('change', () => {
- // Check if this source is locked first
- const foodSource = this.foodSources.find(fs => fs.id === id);
- if (foodSource && foodSource.isLocked) {
- // Reset input to original value and return
- percentageInput.value = foodSource.percentage;
- return;
+ const requestedValue = parseInt(percentageInput.value) || 0;
+ const result = this.validatePercentageChange(id, requestedValue);
+
+ if (result.isValid) {
+ this.applyValidatedChanges(result);
}
-
- let newPercentage = parseInt(percentageInput.value) || 0;
- const maxAllowed = parseInt(percentageInput.dataset.maxAllowed) || 100;
-
- // Constrain to valid range and max allowed
- newPercentage = Math.max(0, Math.min(maxAllowed, newPercentage));
- percentageInput.value = newPercentage;
-
- this.adjustPercentages(id, newPercentage);
- document.getElementById(`percentage-display-${id}`).textContent = `${newPercentage}%`;
+ this.refreshAllPercentageUI();
});
}
@@ -2027,6 +2173,7 @@
foodSource.isLocked = !foodSource.isLocked;
this.updateLockIcon(id);
this.updateLockStates();
+ this.refreshAllPercentageUI();
}
updateLockIcon(id) {
@@ -2064,7 +2211,7 @@
});
// Update percentage constraints based on lock states
- this.updatePercentageConstraints();
+ this.refreshAllPercentageUI();
}
updateFoodSourceData(id, field, value) {