@@ -1100,12 +1461,15 @@
this.isImperial = false;
this.theme = this.getThemeFromURL() || 'system';
this.scale = this.getScaleFromURL() || 1.0;
+ this.foodSources = [];
+ this.maxFoodSources = 5;
this.init();
}
init() {
this.applyTheme();
this.applyScale();
+ this.initializeFoodSources();
this.bindEvents();
this.updateUnitLabels();
this.setupIframeResize();
@@ -1153,13 +1517,289 @@
}
}
+ // Food Source Management Methods
+ initializeFoodSources() {
+ this.addFoodSource();
+ this.updateAddButton();
+ }
+
+ addFoodSource() {
+ if (this.foodSources.length >= this.maxFoodSources) {
+ return;
+ }
+
+ const id = this.generateFoodSourceId();
+ const foodSource = {
+ id: id,
+ name: `Food Source ${this.foodSources.length + 1}`,
+ energy: '',
+ energyUnit: this.isImperial ? 'kcalcup' : 'kcal100g',
+ percentage: this.foodSources.length === 0 ? 100 : 0
+ };
+
+ this.foodSources.push(foodSource);
+ this.redistributePercentages();
+ this.renderFoodSource(foodSource);
+ this.updateAddButton();
+ this.updateFoodCalculations();
+ }
+
+ removeFoodSource(id) {
+ if (this.foodSources.length <= 1) {
+ return; // Cannot remove the last food source
+ }
+
+ const index = this.foodSources.findIndex(fs => fs.id === id);
+ if (index === -1) return;
+
+ this.foodSources.splice(index, 1);
+
+ // Remove the DOM element
+ const element = document.getElementById(`foodSource-${id}`);
+ if (element) {
+ element.remove();
+ }
+
+ // Redistribute percentages among remaining sources
+ this.redistributePercentages();
+ this.updateFoodSourceNames();
+ this.updateAddButton();
+ this.updateFoodCalculations();
+ }
+
+ generateFoodSourceId() {
+ return 'fs_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
+ }
+
+ redistributePercentages() {
+ const count = this.foodSources.length;
+ if (count === 0) return;
+
+ const equalPercentage = Math.floor(100 / count);
+ const remainder = 100 - (equalPercentage * count);
+
+ this.foodSources.forEach((fs, index) => {
+ fs.percentage = equalPercentage + (index < remainder ? 1 : 0);
+ });
+
+ // Update the UI sliders and inputs
+ this.updatePercentageInputs();
+ }
+
+ updatePercentageInputs() {
+ this.foodSources.forEach(fs => {
+ const slider = document.getElementById(`percentage-slider-${fs.id}`);
+ const input = document.getElementById(`percentage-input-${fs.id}`);
+
+ if (slider) slider.value = fs.percentage;
+ if (input) input.value = fs.percentage;
+ });
+ }
+
+ adjustPercentages(changedId, newPercentage) {
+ const changedIndex = this.foodSources.findIndex(fs => fs.id === changedId);
+ if (changedIndex === -1) return;
+
+ const oldPercentage = this.foodSources[changedIndex].percentage;
+ const difference = newPercentage - oldPercentage;
+
+ this.foodSources[changedIndex].percentage = newPercentage;
+
+ // Distribute the difference among other sources proportionally
+ const otherSources = this.foodSources.filter((fs, index) => index !== changedIndex);
+ if (otherSources.length === 0) return;
+
+ const totalOtherPercentage = otherSources.reduce((sum, fs) => sum + fs.percentage, 0);
+
+ if (totalOtherPercentage === 0) {
+ // If all others are 0, distribute equally
+ const remainingPercentage = 100 - newPercentage;
+ const equalShare = Math.floor(remainingPercentage / otherSources.length);
+ const remainder = remainingPercentage - (equalShare * otherSources.length);
+
+ otherSources.forEach((fs, index) => {
+ fs.percentage = equalShare + (index < remainder ? 1 : 0);
+ });
+ } else {
+ // Distribute proportionally
+ const targetTotal = 100 - newPercentage;
+ const scale = targetTotal / totalOtherPercentage;
+
+ let distributedTotal = 0;
+ otherSources.forEach((fs, index) => {
+ if (index === otherSources.length - 1) {
+ // Last item gets the remainder to ensure exact 100%
+ fs.percentage = targetTotal - distributedTotal;
+ } else {
+ fs.percentage = Math.round(fs.percentage * scale);
+ distributedTotal += fs.percentage;
+ }
+ });
+ }
+
+ this.updatePercentageInputs();
+ this.updateFoodCalculations();
+ }
+
+ updateFoodSourceNames() {
+ this.foodSources.forEach((fs, index) => {
+ fs.name = `Food Source ${index + 1}`;
+ const titleElement = document.getElementById(`food-title-${fs.id}`);
+ if (titleElement) {
+ titleElement.textContent = fs.name;
+ }
+ });
+ }
+
+ updateAddButton() {
+ const addBtn = document.getElementById('addFoodBtn');
+ const countSpan = document.getElementById('foodSourceCount');
+
+ if (addBtn) {
+ const remaining = this.maxFoodSources - this.foodSources.length;
+ addBtn.disabled = remaining <= 0;
+
+ if (countSpan) {
+ if (remaining > 0) {
+ countSpan.textContent = `(${remaining} of ${this.maxFoodSources} remaining)`;
+ } else {
+ countSpan.textContent = `(maximum ${this.maxFoodSources} reached)`;
+ }
+ }
+ }
+ }
+
+ renderFoodSource(foodSource) {
+ const container = document.getElementById('foodSources');
+ if (!container) return;
+
+ const cardHTML = `
+
+ `;
+
+ container.insertAdjacentHTML('beforeend', cardHTML);
+
+ // Bind events for the new food source
+ this.bindFoodSourceEvents(foodSource.id);
+ }
+
+ bindFoodSourceEvents(id) {
+ // Energy input events
+ const energyInput = document.getElementById(`energy-${id}`);
+ const energyUnitSelect = document.getElementById(`energy-unit-${id}`);
+ const percentageSlider = document.getElementById(`percentage-slider-${id}`);
+ const percentageInput = document.getElementById(`percentage-input-${id}`);
+ const removeBtn = document.getElementById(`remove-${id}`);
+
+ if (energyInput) {
+ energyInput.addEventListener('input', () => {
+ this.updateFoodSourceData(id, 'energy', energyInput.value);
+ this.updateFoodCalculations();
+ });
+ energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id));
+ }
+
+ if (energyUnitSelect) {
+ energyUnitSelect.addEventListener('change', () => {
+ this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value);
+ this.updateFoodCalculations();
+ });
+ }
+
+ if (percentageSlider) {
+ percentageSlider.addEventListener('input', () => {
+ const newPercentage = parseInt(percentageSlider.value);
+ this.adjustPercentages(id, newPercentage);
+ document.getElementById(`percentage-display-${id}`).textContent = `${newPercentage}%`;
+ });
+ }
+
+ if (percentageInput) {
+ percentageInput.addEventListener('change', () => {
+ const newPercentage = Math.max(0, Math.min(100, parseInt(percentageInput.value) || 0));
+ this.adjustPercentages(id, newPercentage);
+ document.getElementById(`percentage-display-${id}`).textContent = `${newPercentage}%`;
+ });
+ }
+
+ if (removeBtn) {
+ removeBtn.addEventListener('click', () => this.removeFoodSource(id));
+ }
+ }
+
+ updateFoodSourceData(id, field, value) {
+ const foodSource = this.foodSources.find(fs => fs.id === id);
+ if (foodSource) {
+ foodSource[field] = value;
+ }
+ }
+
+ validateFoodSourceEnergy(id) {
+ const energyInput = document.getElementById(`energy-${id}`);
+ const energyUnitSelect = document.getElementById(`energy-unit-${id}`);
+ const errorElement = document.getElementById(`energy-error-${id}`);
+
+ if (!energyInput || !energyUnitSelect || !errorElement) return;
+
+ const energy = parseFloat(energyInput.value);
+ const unit = energyUnitSelect.value;
+
+ let minValue = 1;
+ switch (unit) {
+ case 'kcal100g': minValue = 1; break;
+ case 'kcalkg': minValue = 10; break;
+ case 'kcalcup': minValue = 50; break;
+ case 'kcalcan': minValue = 100; break;
+ }
+
+ if (!this.validateInput(energy, minValue)) {
+ errorElement.classList.remove('dog-calculator-hidden');
+ } else {
+ errorElement.classList.add('dog-calculator-hidden');
+ }
+ }
+
bindEvents() {
const weightInput = document.getElementById('weight');
const dogTypeSelect = document.getElementById('dogType');
- const foodEnergyInput = document.getElementById('foodEnergy');
const daysInput = document.getElementById('days');
const unitSelect = document.getElementById('unit');
const unitToggle = document.getElementById('unitToggle');
+ const addFoodBtn = document.getElementById('addFoodBtn');
if (weightInput) {
weightInput.addEventListener('input', () => this.updateCalorieCalculations());
@@ -1168,14 +1808,6 @@
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
- if (foodEnergyInput) {
- foodEnergyInput.addEventListener('input', () => this.updateFoodCalculations());
- foodEnergyInput.addEventListener('blur', () => this.validateFoodEnergy());
- }
-
- const energyUnitSelect = document.getElementById('energyUnit');
- if (energyUnitSelect) energyUnitSelect.addEventListener('change', () => this.updateFoodCalculations());
-
if (daysInput) {
daysInput.addEventListener('input', () => this.updateFoodCalculations());
daysInput.addEventListener('blur', () => this.validateDays());
@@ -1185,6 +1817,8 @@
if (unitToggle) unitToggle.addEventListener('change', () => this.toggleUnits());
+ if (addFoodBtn) addFoodBtn.addEventListener('click', () => this.addFoodSource());
+
// Modal event listeners
const shareBtn = document.getElementById('shareBtn');
const embedBtn = document.getElementById('embedBtn');
@@ -1248,7 +1882,6 @@
const weightLabel = document.getElementById('weightLabel');
const weightInput = document.getElementById('weight');
const unitSelect = document.getElementById('unit');
- const energyUnitSelect = document.getElementById('energyUnit');
if (metricLabel && imperialLabel) {
metricLabel.classList.toggle('active', !this.isImperial);
@@ -1268,10 +1901,17 @@
'
' +
'
';
}
- // Set energy unit to kcal/cup for imperial
- if (energyUnitSelect && energyUnitSelect.value === 'kcal100g') {
- energyUnitSelect.value = 'kcalcup';
- }
+
+ // Update energy units for all food sources to kcal/cup for imperial
+ this.foodSources.forEach(fs => {
+ if (fs.energyUnit === 'kcal100g') {
+ fs.energyUnit = 'kcalcup';
+ const energyUnitSelect = document.getElementById(`energy-unit-${fs.id}`);
+ if (energyUnitSelect) {
+ energyUnitSelect.value = 'kcalcup';
+ }
+ }
+ });
} else {
if (weightLabel) weightLabel.textContent = "Dog's Weight (kg):";
if (weightInput) {
@@ -1285,10 +1925,17 @@
'
' +
'
';
}
- // Set energy unit to kcal/100g for metric
- if (energyUnitSelect && energyUnitSelect.value === 'kcalcup') {
- energyUnitSelect.value = 'kcal100g';
- }
+
+ // Update energy units for all food sources to kcal/100g for metric
+ this.foodSources.forEach(fs => {
+ if (fs.energyUnit === 'kcalcup') {
+ fs.energyUnit = 'kcal100g';
+ const energyUnitSelect = document.getElementById(`energy-unit-${fs.id}`);
+ if (energyUnitSelect) {
+ energyUnitSelect.value = 'kcal100g';
+ }
+ }
+ });
}
}
@@ -1317,30 +1964,6 @@
return this.isImperial ? weight / 2.20462 : weight;
}
- getFoodEnergyPer100g() {
- const foodEnergyInput = document.getElementById('foodEnergy');
- const energyUnitSelect = document.getElementById('energyUnit');
- if (!foodEnergyInput || !foodEnergyInput.value || !energyUnitSelect) return null;
-
- const energy = parseFloat(foodEnergyInput.value);
- if (isNaN(energy)) return null;
-
- const unit = energyUnitSelect.value;
-
- // Convert all units to kcal/100g for internal calculations
- switch (unit) {
- case 'kcal100g':
- return energy;
- case 'kcalkg':
- return energy / 10; // 1 kg = 10 × 100g
- case 'kcalcup':
- return energy / 1.2; // Assume 1 cup ≈ 120g for dry dog food
- case 'kcalcan':
- return energy / 4.5; // Assume 1 can ≈ 450g for wet dog food
- default:
- return energy;
- }
- }
calculateRER(weightKg) {
return 70 * Math.pow(weightKg, 0.75);
@@ -1397,40 +2020,6 @@
}
}
- validateFoodEnergy() {
- const energyInput = document.getElementById('foodEnergy');
- const energyUnitSelect = document.getElementById('energyUnit');
-
- if (!energyInput || !energyInput.value) {
- this.showError('foodEnergyError', false);
- return;
- }
-
- const energy = parseFloat(energyInput.value);
- const unit = energyUnitSelect?.value || 'kcal100g';
-
- let minValue = 1;
- switch (unit) {
- case 'kcal100g':
- minValue = 1;
- break;
- case 'kcalkg':
- minValue = 10;
- break;
- case 'kcalcup':
- minValue = 50;
- break;
- case 'kcalcan':
- minValue = 100;
- break;
- }
-
- if (!this.validateInput(energy, minValue)) {
- this.showError('foodEnergyError', true);
- } else {
- this.showError('foodEnergyError', false);
- }
- }
validateDays() {
const days = document.getElementById('days')?.value;
@@ -1491,40 +2080,93 @@
const dailyFoodResults = document.getElementById('dailyFoodResults');
const dailyFoodValue = document.getElementById('dailyFoodValue');
const totalFoodDisplay = document.getElementById('totalFoodDisplay');
+ const foodBreakdownResults = document.getElementById('foodBreakdownResults');
+ const foodBreakdownList = document.getElementById('foodBreakdownList');
if (!daysInput || !unitSelect || !dailyFoodResults || !dailyFoodValue || !totalFoodDisplay) {
return;
}
- const energyPer100g = this.getFoodEnergyPer100g();
const days = daysInput.value;
const unit = unitSelect.value;
- this.showError('foodEnergyError', false);
+ // Clear all food source errors first
+ this.foodSources.forEach(fs => {
+ this.showError(`energy-error-${fs.id}`, false);
+ });
this.showError('daysError', false);
-
- if (!energyPer100g || energyPer100g < 0.1) {
- const foodEnergyInput = document.getElementById('foodEnergy');
- if (foodEnergyInput && foodEnergyInput.value) this.showError('foodEnergyError', true);
- dailyFoodResults.style.display = 'none';
- totalFoodDisplay.value = '';
- return;
- }
+ // Validate days input
if (!days || !this.validateInput(days, 1, true)) {
if (days) this.showError('daysError', true);
totalFoodDisplay.value = '';
+ dailyFoodResults.style.display = 'none';
+ if (foodBreakdownResults) foodBreakdownResults.style.display = 'none';
return;
}
const numDays = parseInt(days);
- const dailyFoodGrams = (this.currentMER / energyPer100g) * 100;
- const totalFoodGrams = dailyFoodGrams * numDays;
-
- dailyFoodValue.textContent = this.formatNumber(dailyFoodGrams, 1) + ' g/day';
+ // Calculate per-food breakdown
+ const foodBreakdowns = [];
+ let totalDailyGrams = 0;
+ let hasValidFoods = false;
+
+ this.foodSources.forEach(fs => {
+ const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
+
+ if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) {
+ const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100;
+ const dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100;
+
+ foodBreakdowns.push({
+ name: fs.name,
+ percentage: fs.percentage,
+ dailyGrams: dailyGramsForThisFood,
+ calories: dailyCaloriesForThisFood
+ });
+
+ totalDailyGrams += dailyGramsForThisFood;
+ hasValidFoods = true;
+ }
+ });
+
+ if (!hasValidFoods) {
+ // Show errors for invalid food sources
+ this.foodSources.forEach(fs => {
+ const energyInput = document.getElementById(`energy-${fs.id}`);
+ if (energyInput && energyInput.value && (!this.getFoodSourceEnergyPer100g(fs) || this.getFoodSourceEnergyPer100g(fs) <= 0.1)) {
+ this.showError(`energy-error-${fs.id}`, true);
+ }
+ });
+
+ dailyFoodResults.style.display = 'none';
+ if (foodBreakdownResults) foodBreakdownResults.style.display = 'none';
+ totalFoodDisplay.value = '';
+ return;
+ }
+
+ // Update daily food results (total)
+ dailyFoodValue.textContent = this.formatNumber(totalDailyGrams, 1) + ' g/day';
dailyFoodResults.style.display = 'block';
-
+
+ // Update per-food breakdown
+ if (foodBreakdownList && foodBreakdowns.length > 1) {
+ const breakdownHTML = foodBreakdowns.map(breakdown => `
+
+ ${breakdown.name} (${breakdown.percentage}%):
+ ${this.formatNumber(breakdown.dailyGrams, 1)} g/day
+
+ `).join('');
+
+ foodBreakdownList.innerHTML = breakdownHTML;
+ if (foodBreakdownResults) foodBreakdownResults.style.display = 'block';
+ } else {
+ if (foodBreakdownResults) foodBreakdownResults.style.display = 'none';
+ }
+
+ // Calculate total food amount for the specified days
+ const totalFoodGrams = totalDailyGrams * numDays;
const convertedAmount = this.convertUnits(totalFoodGrams, unit);
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb';
const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : 1;
@@ -1534,6 +2176,29 @@
this.sendHeightToParent();
}
+ getFoodSourceEnergyPer100g(foodSource) {
+ if (!foodSource.energy || !foodSource.energyUnit) return null;
+
+ const energy = parseFloat(foodSource.energy);
+ if (isNaN(energy)) return null;
+
+ const unit = foodSource.energyUnit;
+
+ // Convert all units to kcal/100g for internal calculations
+ switch (unit) {
+ case 'kcal100g':
+ return energy;
+ case 'kcalkg':
+ return energy / 10; // 1 kg = 10 × 100g
+ case 'kcalcup':
+ return energy / 1.2; // Assume 1 cup ≈ 120g for dry dog food
+ case 'kcalcan':
+ return energy / 4.5; // Assume 1 can ≈ 450g for wet dog food
+ default:
+ return energy;
+ }
+ }
+
setupIframeResize() {
// Send height to parent window for iframe auto-resize
this.sendHeightToParent();