/** * Dog Calorie Calculator - iframe version * by Canine Nutrition and Wellness */ class DogCalorieCalculator { constructor() { this.currentMER = 0; this.currentMERMin = 0; // For range calculations this.currentMERMax = 0; // For range calculations this.isImperial = false; this.theme = this.getThemeFromURL() || CALCULATOR_CONFIG.defaultTheme; this.scale = this.getScaleFromURL() || CALCULATOR_CONFIG.defaultScale; this.foodSources = []; this.maxFoodSources = CALCULATOR_CONFIG.maxFoodSources; this.mealsPerDay = 2; this.showPerMeal = false; // Kayafied reference source tracking this.kibbleRefId = null; this.gcRefId = null; this.init(); } init() { this.applyTheme(); this.applyScale(); this.initializeFoodSources(); this.bindEvents(); this.updateUnitLabels(); this.setupIframeResize(); // Show the calculator with fade-in const container = document.getElementById('dogCalculator'); container.classList.add('loaded'); } getThemeFromURL() { const urlParams = new URLSearchParams(window.location.search); const theme = urlParams.get('theme'); return ['light', 'dark', 'system'].includes(theme) ? theme : null; } getScaleFromURL() { const urlParams = new URLSearchParams(window.location.search); const scale = parseFloat(urlParams.get('scale')); return (!isNaN(scale) && scale >= CALCULATOR_CONFIG.minScale && scale <= CALCULATOR_CONFIG.maxScale) ? scale : null; } applyTheme() { const container = document.getElementById('dogCalculator'); container.classList.remove('theme-light', 'theme-dark', 'theme-system'); container.classList.add('theme-' + this.theme); } applyScale() { const container = document.getElementById('dogCalculator'); if (!container) return; // Clamp scale between min and max for usability const clampedScale = Math.max(CALCULATOR_CONFIG.minScale, Math.min(CALCULATOR_CONFIG.maxScale, this.scale)); if (clampedScale !== 1.0) { container.style.transform = `scale(${clampedScale})`; container.style.transformOrigin = 'top center'; // Recalculate height for parent without adding artificial margins setTimeout(() => { this.sendHeightToParent(); }, 100); } } // Food Source Management Methods initializeFoodSources() { // Seed three sources for Kaya's transition const gc = { id: this.generateFoodSourceId(), name: 'Fred & Felia, gently cooked', energy: '115', energyUnit: 'kcal100g', percentage: 5, isLocked: false }; this.foodSources.push(gc); this.renderFoodSource(gc); this.gcRefId = gc.id; const kibble = { id: this.generateFoodSourceId(), name: 'Eukanuba, kibble', energy: '372', energyUnit: 'kcal100g', percentage: 95, isLocked: false }; this.foodSources.push(kibble); this.renderFoodSource(kibble); this.kibbleRefId = kibble.id; const treats = { id: this.generateFoodSourceId(), name: 'Treats', energy: '', energyUnit: 'kcal100g', percentage: 0, isLocked: false }; this.foodSources.push(treats); this.renderFoodSource(treats); this.updateAddButton(); this.updateRemoveButtons(); this.refreshAllPercentageUI(); } 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, isLocked: false }; this.foodSources.push(foodSource); this.redistributePercentages(); this.renderFoodSource(foodSource); this.updateAddButton(); this.updateRemoveButtons(); this.refreshAllPercentageUI(); } 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); // If reference IDs were removed, clear them if (this.kibbleRefId === id) this.kibbleRefId = null; if (this.gcRefId === id) this.gcRefId = null; // 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.updateRemoveButtons(); this.refreshAllPercentageUI(); this.updateCalorieCalculations(); } generateFoodSourceId() { return 'fs_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); } redistributePercentages() { const count = this.foodSources.length; if (count === 0) return; // Only redistribute among unlocked sources const unlockedSources = this.foodSources.filter(fs => !fs.isLocked); const lockedSources = this.foodSources.filter(fs => fs.isLocked); // Calculate total locked percentage const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); // Available percentage for unlocked sources const availablePercentage = 100 - totalLockedPercentage; if (unlockedSources.length > 0) { const equalPercentage = Math.floor(availablePercentage / unlockedSources.length); const remainder = availablePercentage - (equalPercentage * unlockedSources.length); unlockedSources.forEach((fs, index) => { fs.percentage = equalPercentage + (index < remainder ? 1 : 0); }); } // Update the UI sliders and inputs 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}`); 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 after values are set this.updatePercentageConstraints(); } updatePercentageConstraints() { this.foodSources.forEach(fs => { const slider = document.getElementById(`percentage-slider-${fs.id}`); const input = document.getElementById(`percentage-input-${fs.id}`); if (!slider || !input) return; // Always keep full 0-100 scale for all sliders slider.max = 100; input.max = 100; if (fs.isLocked) { // Locked sources can't be changed slider.disabled = true; input.disabled = true; } else { // Calculate the maximum this source can have const lockedSources = this.foodSources.filter(other => other.id !== fs.id && other.isLocked); const totalLockedPercentage = lockedSources.reduce((sum, other) => sum + other.percentage, 0); const maxAllowed = 100 - totalLockedPercentage; // Re-enable slider.disabled = false; input.disabled = false; // Store max allowed for validation (we'll check this in event handlers) slider.dataset.maxAllowed = maxAllowed; input.dataset.maxAllowed = maxAllowed; // If current value exceeds max, adjust it if (fs.percentage > maxAllowed) { fs.percentage = maxAllowed; slider.value = maxAllowed; input.value = maxAllowed; document.getElementById(`percentage-display-${fs.id}`).textContent = `${maxAllowed}%`; } } }); } 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; // Only redistribute among unlocked sources (excluding the changed one) const otherUnlockedSources = this.foodSources.filter((fs, index) => index !== changedIndex && !fs.isLocked ); // If this is the only unlocked source, force it to fill remaining percentage if (otherUnlockedSources.length === 0) { const lockedSources = this.foodSources.filter((fs, index) => index !== changedIndex && fs.isLocked ); const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); const requiredPercentage = 100 - totalLockedPercentage; // Force the changed source to the required percentage this.foodSources[changedIndex].percentage = requiredPercentage; this.updatePercentageInputs(); this.updateFoodCalculations(); return; } // Calculate total locked percentage (excluding the changed source) const lockedSources = this.foodSources.filter((fs, index) => index !== changedIndex && fs.isLocked ); const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); // Available percentage for unlocked sources const availablePercentage = 100 - newPercentage - totalLockedPercentage; const totalUnlockedPercentage = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0); if (totalUnlockedPercentage === 0) { // If all other unlocked sources are 0, distribute equally const equalShare = Math.floor(availablePercentage / otherUnlockedSources.length); const remainder = availablePercentage - (equalShare * otherUnlockedSources.length); otherUnlockedSources.forEach((fs, index) => { fs.percentage = equalShare + (index < remainder ? 1 : 0); }); } else { // Distribute proportionally among unlocked sources const scale = availablePercentage / totalUnlockedPercentage; let distributedTotal = 0; otherUnlockedSources.forEach((fs, index) => { if (index === otherUnlockedSources.length - 1) { // Last item gets the remainder to ensure exact 100% fs.percentage = availablePercentage - distributedTotal; } else { fs.percentage = Math.round(fs.percentage * scale); distributedTotal += fs.percentage; } }); } 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) => { // Only update if the name is still the default pattern if (fs.name.match(/^Food Source \d+$/)) { fs.name = `Food Source ${index + 1}`; const titleElement = document.getElementById(`food-title-${fs.id}`); if (titleElement) { titleElement.value = fs.name; } } }); } updateAddButton() { const addBtn = document.getElementById('addFoodBtn'); if (addBtn) { const remaining = this.maxFoodSources - this.foodSources.length; const buttonText = addBtn.querySelector('span:last-child'); if (remaining <= 0) { // Disable button and show max reached message addBtn.disabled = true; if (buttonText) { buttonText.textContent = `Maximum ${this.maxFoodSources} sources reached`; } } else { // Enable button with normal text addBtn.disabled = false; if (buttonText) { buttonText.textContent = 'Add another food source'; } } } } updateRemoveButtons() { // Show/hide remove buttons based on whether we have more than one source const hasMultipleSources = this.foodSources.length > 1; this.foodSources.forEach(fs => { const removeBtn = document.getElementById(`remove-${fs.id}`); if (removeBtn) { removeBtn.style.display = hasMultipleSources ? 'block' : 'none'; } }); } renderFoodSource(foodSource) { const container = document.getElementById('foodSources'); if (!container) return; const cardHTML = `
Please enter a valid energy content
`; container.insertAdjacentHTML('beforeend', cardHTML); // Bind events for the new food source this.bindFoodSourceEvents(foodSource.id); } bindFoodSourceEvents(id) { // Name input events const nameInput = document.getElementById(`food-title-${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}`); const lockBtn = document.getElementById(`lock-${id}`); if (nameInput) { nameInput.addEventListener('input', () => { const newName = nameInput.value.trim() || `Food Source ${this.foodSources.findIndex(fs => fs.id === id) + 1}`; this.updateFoodSourceData(id, 'name', newName); this.updateFoodCalculations(); // This will refresh the food amount breakdown with new names }); nameInput.addEventListener('blur', () => { // If field is empty, restore default name if (!nameInput.value.trim()) { const defaultName = `Food Source ${this.foodSources.findIndex(fs => fs.id === id) + 1}`; nameInput.value = defaultName; this.updateFoodSourceData(id, 'name', defaultName); this.updateFoodCalculations(); } }); } if (energyInput) { energyInput.addEventListener('input', () => { this.updateFoodSourceData(id, 'energy', energyInput.value); // If kibble reference changed, recompute daily target if (id === this.kibbleRefId) { this.updateCalorieCalculations(); } // Auto-select cups when entering energy for kcal/cup const foodSource = this.foodSources.find(fs => fs.id === id); if (foodSource && foodSource.energyUnit === 'kcalcup' && parseFloat(energyInput.value) > 0) { // Cups display removed; default to grams const unitSelect = document.getElementById('unit'); if (unitSelect) { unitSelect.value = 'g'; unitSelect.setAttribute('value', 'g'); this.setActiveUnitButton('g'); } } this.updateFoodCalculations(); }); energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id)); } if (energyUnitSelect) { energyUnitSelect.addEventListener('change', () => { this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value); if (id === this.kibbleRefId) { this.updateCalorieCalculations(); } // Auto-select the most appropriate unit based on energy unit const unitSelect = document.getElementById('unit'); const energyInput = document.getElementById(`energy-${id}`); if (unitSelect) { switch(energyUnitSelect.value) { case 'kcalcup': // Cups display not available; default to grams unitSelect.value = 'g'; this.setActiveUnitButton('g'); this.updateFoodCalculations(); break; case 'kcal100g': // For kcal/100g, select grams unitSelect.value = 'g'; this.setActiveUnitButton('g'); this.updateFoodCalculations(); break; case 'kcalkg': // For kcal/kg, also select grams (or could be kg) unitSelect.value = 'g'; this.setActiveUnitButton('g'); this.updateFoodCalculations(); break; case 'kcalcan': // For kcal/can, use grams as default unitSelect.value = 'g'; this.setActiveUnitButton('g'); this.updateFoodCalculations(); break; } } else { // No unit select, just update calculations this.updateFoodCalculations(); } }); } if (percentageSlider) { percentageSlider.addEventListener('input', () => { const requestedValue = parseInt(percentageSlider.value); const result = this.validatePercentageChange(id, requestedValue); if (result.isValid) { this.applyValidatedChanges(result); } // Always refresh to ensure valid state this.refreshAllPercentageUI(); }); } if (percentageInput) { percentageInput.addEventListener('change', () => { const requestedValue = parseInt(percentageInput.value) || 0; const result = this.validatePercentageChange(id, requestedValue); if (result.isValid) { this.applyValidatedChanges(result); } this.refreshAllPercentageUI(); }); } if (removeBtn) { removeBtn.addEventListener('click', () => this.removeFoodSource(id)); } if (lockBtn) { lockBtn.addEventListener('click', () => this.toggleLock(id)); } } toggleLock(id) { const foodSource = this.foodSources.find(fs => fs.id === id); if (!foodSource) return; // Check if we're trying to lock the last unlocked source const unlockedSources = this.foodSources.filter(fs => !fs.isLocked); if (unlockedSources.length === 1 && unlockedSources[0].id === id) { // Cannot lock the last unlocked source alert('At least one food source must remain flexible for percentage adjustments.'); return; } // Toggle lock state foodSource.isLocked = !foodSource.isLocked; this.updateLockIcon(id); this.updateLockStates(); this.refreshAllPercentageUI(); } updateLockIcon(id) { const foodSource = this.foodSources.find(fs => fs.id === id); const lockIcon = document.getElementById(`lock-${id}`); if (!lockIcon || !foodSource) return; if (foodSource.isLocked) { lockIcon.classList.remove('unlocked'); lockIcon.classList.add('locked'); lockIcon.title = 'Unlock this percentage'; } else { lockIcon.classList.remove('locked'); lockIcon.classList.add('unlocked'); lockIcon.title = 'Lock this percentage'; } } updateLockStates() { const unlockedSources = this.foodSources.filter(fs => !fs.isLocked); // Update lock icon states - disable lock for last unlocked source this.foodSources.forEach(fs => { const lockIcon = document.getElementById(`lock-${fs.id}`); if (lockIcon) { if (!fs.isLocked && unlockedSources.length === 1) { lockIcon.classList.add('disabled'); lockIcon.title = 'Cannot lock - at least one source must remain flexible'; } else { lockIcon.classList.remove('disabled'); lockIcon.title = fs.isLocked ? 'Unlock this percentage' : 'Lock this percentage'; } } }); // Update percentage constraints based on lock states this.refreshAllPercentageUI(); } 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 ageInput = document.getElementById('ageMonths'); 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()); weightInput.addEventListener('blur', () => this.validateWeight()); } if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations()); // Kayafied: age input drives energy target if (ageInput) { ageInput.addEventListener('input', () => this.updateCalorieCalculations()); ageInput.addEventListener('blur', () => this.updateCalorieCalculations()); } if (daysInput) { daysInput.addEventListener('input', () => { this.updateDayLabel(); this.updateFoodCalculations(); }); daysInput.addEventListener('blur', () => this.validateDays()); } if (unitSelect) unitSelect.addEventListener('change', () => this.updateFoodCalculations()); // Unit button event listeners const unitButtons = document.querySelectorAll('.dog-calculator-unit-btn'); unitButtons.forEach(button => { button.addEventListener('click', (e) => { const selectedUnit = e.target.dataset.unit; this.setActiveUnitButton(selectedUnit); // Update hidden select to trigger existing logic if (unitSelect) { unitSelect.value = selectedUnit; this.updateFoodCalculations(); } }); }); if (unitToggle) unitToggle.addEventListener('change', () => this.toggleUnits()); if (addFoodBtn) addFoodBtn.addEventListener('click', () => this.addFoodSource()); // Feeding configuration event listeners const showDaily = document.getElementById('showDaily'); const showPerMeal = document.getElementById('showPerMeal'); const mealsPerDayInput = document.getElementById('mealsPerDay'); const mealInputGroup = document.getElementById('mealInputGroup'); if (showDaily) { showDaily.addEventListener('change', () => { if (showDaily.checked) { this.showPerMeal = false; if (mealInputGroup) mealInputGroup.style.display = 'none'; this.updateDayLabel(); this.updateFoodCalculations(); } }); } if (showPerMeal) { showPerMeal.addEventListener('change', () => { if (showPerMeal.checked) { this.showPerMeal = true; if (mealInputGroup) mealInputGroup.style.display = 'inline-flex'; this.updateDayLabel(); this.updateFoodCalculations(); } }); } if (mealsPerDayInput) { mealsPerDayInput.addEventListener('input', () => { const meals = parseInt(mealsPerDayInput.value); if (meals && meals >= 1 && meals <= 10) { this.mealsPerDay = meals; if (this.showPerMeal) { this.updateDayLabel(); this.updateFoodCalculations(); } } }); mealsPerDayInput.addEventListener('blur', () => { if (!mealsPerDayInput.value || parseInt(mealsPerDayInput.value) < 1) { mealsPerDayInput.value = 2; this.mealsPerDay = 2; if (this.showPerMeal) { this.updateDayLabel(); this.updateFoodCalculations(); } } }); } // Modal event listeners const shareBtn = document.getElementById('shareBtn'); const shareModalClose = document.getElementById('shareModalClose'); if (shareBtn) shareBtn.addEventListener('click', () => this.showShareModal()); if (shareModalClose) shareModalClose.addEventListener('click', () => this.hideShareModal()); // Share buttons const shareFacebook = document.getElementById('shareFacebook'); const shareTwitter = document.getElementById('shareTwitter'); const shareLinkedIn = document.getElementById('shareLinkedIn'); const shareEmail = document.getElementById('shareEmail'); const shareCopy = document.getElementById('shareCopy'); if (shareFacebook) shareFacebook.addEventListener('click', () => this.shareToFacebook()); if (shareTwitter) shareTwitter.addEventListener('click', () => this.shareToTwitter()); if (shareLinkedIn) shareLinkedIn.addEventListener('click', () => this.shareToLinkedIn()); if (shareEmail) shareEmail.addEventListener('click', () => this.shareViaEmail()); if (shareCopy) shareCopy.addEventListener('click', () => this.copyShareLink()); // Embed copy buttons removed (embedding disabled) // Close modals on outside click const shareModal = document.getElementById('shareModal'); if (shareModal) { shareModal.addEventListener('click', (e) => { if (e.target === shareModal) this.hideShareModal(); }); } // Embed modal removed } toggleUnits() { const toggle = document.getElementById('unitToggle'); this.isImperial = toggle.checked; this.updateUnitLabels(); this.convertExistingValues(); this.updateCalorieCalculations(); } updateUnitLabels() { const metricLabel = document.getElementById('metricLabel'); const imperialLabel = document.getElementById('imperialLabel'); const weightLabel = document.getElementById('weightLabel'); const weightInput = document.getElementById('weight'); const unitSelect = document.getElementById('unit'); if (metricLabel && imperialLabel) { metricLabel.classList.toggle('active', !this.isImperial); imperialLabel.classList.toggle('active', this.isImperial); } // Kaya: restrict to metric g/kg only if (unitSelect) { unitSelect.innerHTML = '' + ''; if (!unitSelect.value || (unitSelect.value !== 'g' && unitSelect.value !== 'kg')) { unitSelect.value = 'g'; this.setActiveUnitButton('g'); } } } convertExistingValues() { const weightInput = document.getElementById('weight'); if (weightInput && weightInput.value) { const currentWeight = parseFloat(weightInput.value); if (!isNaN(currentWeight)) { if (this.isImperial) { weightInput.value = this.formatNumber(currentWeight * 2.20462, 1); } else { weightInput.value = this.formatNumber(currentWeight / 2.20462, 1); } } } } getWeightInKg() { const weightInput = document.getElementById('weight'); if (!weightInput || !weightInput.value) return null; const weight = parseFloat(weightInput.value); if (isNaN(weight)) return null; return this.isImperial ? weight / 2.20462 : weight; } calculateRER(weightKg) { return 70 * Math.pow(weightKg, 0.75); } calculateMER(rer, factor) { return rer * factor; } // Get the range multipliers for each life stage getLifeStageRange(factor) { // Define ranges based on the reference image const ranges = { '3.0': { min: 3.0, max: 3.0 }, // Puppy 0-4 months (no range) '2.0': { min: 2.0, max: 2.0 }, // Puppy 4m-adult OR Working light (no range for puppies) '1.2': { min: 1.2, max: 1.4 }, // Adult inactive/obese '1.6': { min: 1.4, max: 1.6 }, // Adult neutered/spayed '1.8': { min: 1.6, max: 1.8 }, // Adult intact '1.0': { min: 1.0, max: 1.0 }, // Weight loss (fixed) '1.7': { min: 1.2, max: 1.8 }, // Weight gain (wide range) '5.0': { min: 5.0, max: 5.0 }, // Working heavy (upper bound) '1.1': { min: 1.1, max: 1.1 } // Senior (no range) }; const key = factor.toFixed(1); return ranges[key] || { min: factor, max: factor }; } validateInput(value, min = 0, isInteger = false) { const num = parseFloat(value); if (isNaN(num) || num < min) return false; if (isInteger && !Number.isInteger(num)) return false; return true; } showError(elementId, show = true) { const errorElement = document.getElementById(elementId); if (errorElement) { if (show) { errorElement.classList.remove('dog-calculator-hidden'); } else { errorElement.classList.add('dog-calculator-hidden'); } } } convertUnits(grams, unit, foodSource = null) { switch (unit) { case 'kg': return grams / 1000; case 'oz': return grams / 28.3495; case 'lb': return grams / 453.592; case 'cups': // For cups, we need to convert from grams worth of calories to cups if (foodSource && foodSource.energyUnit === 'kcalcup' && foodSource.energy) { // Get calories per 100g for this food const caloriesPerGram = this.getFoodSourceEnergyPer100g(foodSource) / 100; // Calculate total calories represented by these grams const totalCalories = grams * caloriesPerGram; // Divide by calories per cup to get number of cups const caloriesPerCup = parseFloat(foodSource.energy); return totalCalories / caloriesPerCup; } return null; // Cannot convert to cups without kcal/cup default: return grams; } } formatNumber(num, decimals = 0) { if (decimals === 0) { return Math.round(num).toString(); } return num.toFixed(decimals).replace(/\.?0+$/, ''); } validateWeight() { const weightKg = this.getWeightInKg(); if (weightKg !== null && weightKg < 0.1) { this.showError('weightError', true); } else { this.showError('weightError', false); } } validateDays() { const days = document.getElementById('days')?.value; if (days && !this.validateInput(days, 1, true)) { this.showError('daysError', true); } else { this.showError('daysError', false); } } updateDayLabel() { const days = document.getElementById('days')?.value; const dayLabel = document.getElementById('dayLabel'); const mealNote = document.getElementById('mealNote'); if (dayLabel && days) { const numDays = parseInt(days); dayLabel.textContent = numDays === 1 ? 'day' : 'days'; } if (mealNote) { if (this.showPerMeal && days) { const numDays = parseInt(days); const totalMeals = numDays * this.mealsPerDay; mealNote.textContent = ` (${totalMeals} meal${totalMeals === 1 ? '' : 's'} total)`; mealNote.style.display = 'inline'; } else { mealNote.style.display = 'none'; } } } setActiveUnitButton(unit) { const unitButtons = document.querySelectorAll('.dog-calculator-unit-btn'); unitButtons.forEach(button => { button.classList.remove('active'); if (button.dataset.unit === unit) { button.classList.add('active'); } }); } updateCalorieCalculations() { // Kaya-specific: compute daily kcal target from age + kibble energy const ageInput = document.getElementById('ageMonths'); const ageClampNote = document.getElementById('ageClampNote'); // Default: hide clamp note if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden'); if (!ageInput || !ageInput.value) { this.currentMER = 0; this.currentMERMin = 0; this.currentMERMax = 0; return; } let age = parseFloat(ageInput.value); if (isNaN(age)) { this.currentMER = 0; this.currentMERMin = 0; this.currentMERMax = 0; return; } // Clamp age to [2, 12] if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); } if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); } // Bucket pills removed // Calculate interpolated kibble grams/day for 30 kg at this age const kibbleGrams = this.getKayaKibbleGramsForAge(age); // Get kibble reference energy density in kcal/100g let kibbleEnergyPer100g = null; if (this.kibbleRefId) { const kibbleRef = this.foodSources.find(fs => fs.id === this.kibbleRefId); kibbleEnergyPer100g = kibbleRef ? this.getFoodSourceEnergyPer100g(kibbleRef) : null; } if (!kibbleEnergyPer100g || kibbleEnergyPer100g <= 0) { // Cannot compute without kibble energy density this.currentMER = 0; this.currentMERMin = 0; this.currentMERMax = 0; return; } const kcalPerGram = kibbleEnergyPer100g / 100.0; const dailyKcal = kibbleGrams * kcalPerGram; this.currentMER = dailyKcal; this.currentMERMin = dailyKcal; this.currentMERMax = dailyKcal; this.updateFoodCalculations(); } // Kaya: monthly table with interpolation between months getKayaKibbleGramsForAge(ageMonths) { // Precomputed monthly values for 30 kg (g/day) const table = { 2: 250, 3: 330, 4: 365, 5: 382, 6: 400, 7: 405, 8: 410, 9: 410, 10: 410, 11: 408, 12: 405 }; // Exact integer month const lower = Math.floor(ageMonths); const upper = Math.ceil(ageMonths); if (lower === upper) return table[lower] || 0; // Linear interpolation between bounds const lowerVal = table[lower] || 0; const upperVal = table[upper] || 0; const t = (ageMonths - lower) / (upper - lower); return lowerVal + (upperVal - lowerVal) * t; } updateCupsButtonState() { const cupsButton = document.getElementById('cupsButton'); if (!cupsButton) return; // Check if any food source has kcal/cup selected const hasKcalCup = this.foodSources.some(fs => fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0 ); if (hasKcalCup) { cupsButton.disabled = false; cupsButton.title = 'Show amounts in cups'; } else { cupsButton.disabled = true; cupsButton.title = 'Available when using kcal/cup measurement'; // If cups was selected, switch back to grams const unitSelect = document.getElementById('unit'); if (unitSelect && unitSelect.value === 'cups') { unitSelect.value = 'g'; this.setActiveUnitButton('g'); } } } updateFoodCalculations() { if (this.currentMER === 0) return; // Check if we have a range const hasRange = this.currentMERMin !== this.currentMERMax; const daysInput = document.getElementById('days'); const unitSelect = document.getElementById('unit'); const dailyFoodResults = document.getElementById('dailyFoodResults'); const dailyFoodValue = document.getElementById('dailyFoodValue'); const foodAmountsSection = document.getElementById('foodAmountsSection'); const foodAmountsList = document.getElementById('foodAmountsList'); const totalAmountDisplay = document.getElementById('totalAmountDisplay'); const foodBreakdownResults = document.getElementById('foodBreakdownResults'); const foodBreakdownList = document.getElementById('foodBreakdownList'); const feedingConfig = document.getElementById('feedingConfig'); // Update cups button state this.updateCupsButtonState(); if (!daysInput || !unitSelect || !dailyFoodResults || !dailyFoodValue || !foodAmountsSection) { return; } const days = daysInput.value; let unit = unitSelect.value; // Failsafe: if unit is empty string but cups button is active, use 'cups' if (!unit || unit === '') { const activeButton = document.querySelector('.dog-calculator-unit-btn.active'); if (activeButton) { unit = activeButton.dataset.unit || 'g'; } else { unit = 'g'; // Default fallback } } const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : unit === 'lb' ? 'lb' : 'cups'; const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : unit === 'cups' ? 1 : 1; // Debug: log what unit is being used console.log('UpdateFoodCalculations - unit:', unit, 'unitLabel:', unitLabel); // Determine frequency suffix for display const frequencySuffix = this.showPerMeal ? '/meal' : '/day'; // Clear all food source errors first this.foodSources.forEach(fs => { this.showError(`energy-error-${fs.id}`, false); }); this.showError('daysError', false); // Validate days input if (!days || !this.validateInput(days, 1, true)) { if (days) this.showError('daysError', true); foodAmountsSection.style.display = 'none'; dailyFoodResults.style.display = 'none'; if (foodBreakdownResults) foodBreakdownResults.style.display = 'none'; if (feedingConfig) feedingConfig.style.display = 'none'; // Hide unit buttons when validation fails const unitButtons = document.getElementById('unitButtons'); if (unitButtons) unitButtons.style.display = 'none'; return; } const numDays = parseInt(days); // 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; // Calculate range values if applicable const dailyCaloriesMin = hasRange ? (this.currentMERMin * fs.percentage) / 100 : dailyCaloriesForThisFood; const dailyCaloriesMax = hasRange ? (this.currentMERMax * fs.percentage) / 100 : dailyCaloriesForThisFood; let dailyGramsForThisFood; let dailyGramsMin, dailyGramsMax; let dailyCupsForThisFood = null; let dailyCupsMin, dailyCupsMax; // For kcal/cup, calculate cups directly from calories if (fs.energyUnit === 'kcalcup' && fs.energy) { const caloriesPerCup = parseFloat(fs.energy); dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup; dailyCupsMin = dailyCaloriesMin / caloriesPerCup; dailyCupsMax = dailyCaloriesMax / caloriesPerCup; // We still need grams for total calculation, use approximation dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } else { // For other units, calculate grams normally dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; } // Calculate per-meal amounts if needed const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood; const displayGramsMin = this.showPerMeal ? dailyGramsMin / this.mealsPerDay : dailyGramsMin; const displayGramsMax = this.showPerMeal ? dailyGramsMax / this.mealsPerDay : dailyGramsMax; const displayCups = dailyCupsForThisFood !== null ? (this.showPerMeal ? dailyCupsForThisFood / this.mealsPerDay : dailyCupsForThisFood) : null; const displayCupsMin = dailyCupsMin !== undefined ? (this.showPerMeal ? dailyCupsMin / this.mealsPerDay : dailyCupsMin) : null; const displayCupsMax = dailyCupsMax !== undefined ? (this.showPerMeal ? dailyCupsMax / this.mealsPerDay : dailyCupsMax) : null; const displayCalories = this.showPerMeal ? dailyCaloriesForThisFood / this.mealsPerDay : dailyCaloriesForThisFood; const displayCaloriesMin = this.showPerMeal ? dailyCaloriesMin / this.mealsPerDay : dailyCaloriesMin; const displayCaloriesMax = this.showPerMeal ? dailyCaloriesMax / this.mealsPerDay : dailyCaloriesMax; foodBreakdowns.push({ name: fs.name, percentage: fs.percentage, dailyGrams: dailyGramsForThisFood, dailyGramsMin: dailyGramsMin, dailyGramsMax: dailyGramsMax, displayGrams: displayGrams, displayGramsMin: displayGramsMin, displayGramsMax: displayGramsMax, dailyCups: dailyCupsForThisFood, dailyCupsMin: dailyCupsMin, dailyCupsMax: dailyCupsMax, displayCups: displayCups, displayCupsMin: displayCupsMin, displayCupsMax: displayCupsMax, calories: dailyCaloriesForThisFood, displayCalories: displayCalories, displayCaloriesMin: displayCaloriesMin, displayCaloriesMax: displayCaloriesMax, isLocked: fs.isLocked, hasEnergyContent: true, hasRange: hasRange, foodSource: fs // Store reference for cups conversion }); totalDailyGrams += dailyGramsForThisFood; hasValidFoods = true; } else if (fs.percentage > 0) { // Include food sources without energy content but show them as needing energy content foodBreakdowns.push({ name: fs.name, percentage: fs.percentage, dailyGrams: 0, displayGrams: 0, dailyCups: null, displayCups: null, calories: 0, displayCalories: 0, isLocked: fs.isLocked, hasEnergyContent: false, foodSource: fs // Store reference for cups conversion }); } }); 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'; if (feedingConfig) feedingConfig.style.display = 'none'; // Hide unit buttons when no valid foods const unitButtons = document.getElementById('unitButtons'); if (unitButtons) unitButtons.style.display = 'none'; // If we have any food sources without energy content, still show the breakdown section if (foodBreakdowns.length > 0) { // Show food amounts section with warnings for missing energy content const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb'; const foodAmountsHTML = foodBreakdowns.map(breakdown => { const lockIndicator = breakdown.isLocked ? '🔒' : ''; return `
${breakdown.name} ${breakdown.percentage}% ${lockIndicator}
⚠️
`; }).join(''); if (foodAmountsList) { foodAmountsList.innerHTML = foodAmountsHTML; } if (totalAmountDisplay) { totalAmountDisplay.textContent = "Enter energy content for all foods"; } foodAmountsSection.style.display = 'block'; this.sendHeightToParent(); } else { foodAmountsSection.style.display = 'none'; } return; } // Update daily food results (total) - will be updated with proper units later dailyFoodResults.style.display = 'block'; // Show feeding configuration when we have valid foods if (feedingConfig) { feedingConfig.style.display = 'block'; // Ensure "Per day" is checked when feeding config becomes visible const showDaily = document.getElementById('showDaily'); if (showDaily && !showDaily.checked && !document.getElementById('showPerMeal').checked) { showDaily.checked = true; } } // Show unit buttons when daily results are shown const unitButtons = document.getElementById('unitButtons'); if (unitButtons) unitButtons.style.display = 'flex'; // Update per-food breakdown if (foodBreakdownList && foodBreakdowns.length > 1) { const breakdownHTML = foodBreakdowns.map(breakdown => { let valueContent; if (breakdown.hasEnergyContent) { if (unit === 'cups') { // For cups, use the pre-calculated cups value if available if (breakdown.displayCups !== null) { if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) { valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; } else { valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; } } else { valueContent = `N/A`; } } else { // For other units (g, kg, oz, lb) if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) { const minConverted = this.convertUnits(breakdown.displayGramsMin, unit); const maxConverted = this.convertUnits(breakdown.displayGramsMax, unit); valueContent = `${this.formatNumber(minConverted, decimals)}-${this.formatNumber(maxConverted, decimals)} ${unitLabel}${frequencySuffix}`; } else { valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; } } } else { valueContent = `⚠️`; } return `
${breakdown.name} (${breakdown.percentage}%${breakdown.isLocked ? ' - locked' : ''}): ${valueContent}
`; }).join(''); foodBreakdownList.innerHTML = breakdownHTML; if (foodBreakdownResults) foodBreakdownResults.style.display = 'block'; } else { if (foodBreakdownResults) foodBreakdownResults.style.display = 'none'; } // Generate individual food amount breakdown // Update daily food value with correct units const displayTotal = this.showPerMeal ? totalDailyGrams / this.mealsPerDay : totalDailyGrams; let convertedTotal; let totalDisplayText; if (unit === 'cups') { console.log('Unit is cups, checking validity...'); // For cups, we can only show total if all foods with percentage > 0 have kcal/cup const validForCups = foodBreakdowns.filter(b => b.percentage > 0) .every(b => b.displayCups !== null && b.displayCups !== undefined); console.log('Valid for cups?', validForCups, 'Breakdowns:', foodBreakdowns); if (validForCups) { // Calculate total cups using pre-calculated values let totalCups = 0; let totalCupsMin = 0; let totalCupsMax = 0; foodBreakdowns.forEach(breakdown => { if (breakdown.percentage > 0 && breakdown.displayCups !== null) { totalCups += breakdown.displayCups; if (breakdown.hasRange) { totalCupsMin += breakdown.displayCupsMin || breakdown.displayCups; totalCupsMax += breakdown.displayCupsMax || breakdown.displayCups; } else { totalCupsMin += breakdown.displayCups; totalCupsMax += breakdown.displayCups; } } }); if (hasRange && totalCupsMin !== totalCupsMax) { totalDisplayText = `${this.formatNumber(totalCupsMin, decimals)}-${this.formatNumber(totalCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; } else { totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; } } else { totalDisplayText = 'Mixed units - see breakdown'; } } else { // Calculate totals for ranges if (hasRange) { let totalGramsMin = 0; let totalGramsMax = 0; foodBreakdowns.forEach(breakdown => { if (breakdown.percentage > 0 && breakdown.hasEnergyContent) { totalGramsMin += breakdown.displayGramsMin || breakdown.displayGrams; totalGramsMax += breakdown.displayGramsMax || breakdown.displayGrams; } }); const convertedMin = this.convertUnits(totalGramsMin, unit); const convertedMax = this.convertUnits(totalGramsMax, unit); if (totalGramsMin !== totalGramsMax) { totalDisplayText = `${this.formatNumber(convertedMin, decimals)}-${this.formatNumber(convertedMax, decimals)} ${unitLabel}${frequencySuffix}`; } else { totalDisplayText = this.formatNumber(convertedMin, decimals) + ` ${unitLabel}${frequencySuffix}`; } } else { convertedTotal = this.convertUnits(displayTotal, unit); totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; } } dailyFoodValue.textContent = totalDisplayText; // Build HTML for individual food amounts const foodAmountsHTML = foodBreakdowns.map(breakdown => { const lockIndicator = breakdown.isLocked ? '🔒' : ''; if (!breakdown.hasEnergyContent) { // Show warning for food sources without energy content return `
${breakdown.name} ${breakdown.percentage}% ${lockIndicator}
⚠️
`; } else { // For multi-day calculations: show total amount for all days let amountDisplay; if (unit === 'cups') { // For cups, use pre-calculated cups value if (breakdown.dailyCups !== null) { const totalCupsForDays = breakdown.dailyCups * numDays; amountDisplay = `${this.formatNumber(totalCupsForDays, decimals)} ${unitLabel}`; } else { amountDisplay = `N/A`; } } else { // For other units, calculate from grams const totalGramsForDays = this.showPerMeal ? (breakdown.dailyGrams / this.mealsPerDay) * numDays * this.mealsPerDay : breakdown.dailyGrams * numDays; const convertedAmount = this.convertUnits(totalGramsForDays, unit); amountDisplay = `${this.formatNumber(convertedAmount, decimals)} ${unitLabel}`; } return `
${breakdown.name} ${breakdown.percentage}% ${lockIndicator}
${amountDisplay}
`; } }).join(''); // Calculate and display total const totalFoodGrams = totalDailyGrams * numDays; // Update the display if (foodAmountsList) { foodAmountsList.innerHTML = foodAmountsHTML; } if (totalAmountDisplay) { if (unit === 'cups') { // For cups total, check if all foods can be converted const validForCups = foodBreakdowns.filter(b => b.percentage > 0) .every(b => b.dailyCups !== null && b.dailyCups !== undefined); if (validForCups) { // Calculate total cups using pre-calculated values let totalCups = 0; foodBreakdowns.forEach(breakdown => { if (breakdown.percentage > 0 && breakdown.dailyCups !== null) { totalCups += breakdown.dailyCups * numDays; } }); totalAmountDisplay.textContent = `${this.formatNumber(totalCups, decimals)} ${unitLabel}`; } else { totalAmountDisplay.textContent = 'Mixed units - see individual amounts'; } } else { const totalConverted = this.convertUnits(totalFoodGrams, unit); totalAmountDisplay.textContent = `${this.formatNumber(totalConverted, decimals)} ${unitLabel}`; } } foodAmountsSection.style.display = 'block'; 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; } } // Iframe auto-resize for allowed embeddings setupIframeResize() { // Only when embedded in an iframe if (window.top === window.self) return; // Initial send once UI is ready setTimeout(() => this.sendHeightToParent(), 50); // Monitor for content/attribute changes const observer = new MutationObserver(() => { clearTimeout(this._resizeTimer); this._resizeTimer = setTimeout(() => this.sendHeightToParent(), 100); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); // On viewport resize window.addEventListener('resize', () => this.sendHeightToParent()); } sendHeightToParent() { if (!(window.parent && window.parent !== window)) return; const container = document.getElementById('dogCalculator'); // Prefer visual height including transform scaling let height = 0; if (container) { const rect = container.getBoundingClientRect(); height = Math.ceil(rect.height); } else { height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); } window.parent.postMessage({ type: 'dogCalculatorResize', height: height }, '*'); } // Modal functionality showShareModal() { const modal = document.getElementById('shareModal'); const shareUrl = document.getElementById('shareUrl'); if (modal && shareUrl) { shareUrl.value = window.location.href; // Use flex so content is centered within modal viewport modal.style.display = 'flex'; // Sync modal scroll position with current page scroll so content is visible try { modal.scrollTop = window.scrollY || document.documentElement.scrollTop || 0; } catch (e) {} // Ensure the modal is visible even when the page is scrolled // by recalculating parent iframe height (defensive) this.sendHeightToParent(); } } hideShareModal() { const modal = document.getElementById('shareModal'); if (modal) modal.style.display = 'none'; this.sendHeightToParent(); } // Embed modal removed (embedding disabled) // Embed modal removed (embedding disabled) shareToFacebook() { const url = encodeURIComponent(window.location.href); window.open('https://www.facebook.com/sharer/sharer.php?u=' + url, '_blank', 'width=600,height=400'); } shareToTwitter() { const url = encodeURIComponent(window.location.href); const text = encodeURIComponent('Check out this useful dog calorie calculator!'); window.open('https://twitter.com/intent/tweet?url=' + url + '&text=' + text, '_blank', 'width=600,height=400'); } shareToLinkedIn() { const url = encodeURIComponent(window.location.href); window.open('https://www.linkedin.com/sharing/share-offsite/?url=' + url, '_blank', 'width=600,height=400'); } shareViaEmail() { const subject = encodeURIComponent('Dog Calorie Calculator'); const body = encodeURIComponent('Check out this useful dog calorie calculator: ' + window.location.href); window.location.href = 'mailto:?subject=' + subject + '&body=' + body; } async copyShareLink() { const shareUrl = document.getElementById('shareUrl'); const copyBtn = document.getElementById('shareCopy'); if (shareUrl && copyBtn) { try { await navigator.clipboard.writeText(shareUrl.value); const originalText = copyBtn.textContent; copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = originalText; copyBtn.classList.remove('copied'); }, 2000); } catch (err) { // Fallback for older browsers shareUrl.select(); document.execCommand('copy'); } } } // Embed code copy removed (embedding disabled) } // Initialize calculator when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Allow embedding only from approved parent hosts if (window.top !== window.self) { const allowedHosts = ['caninenutritionandwellness.com', 'www.caninenutritionandwellness.com']; let parentAllowed = false; // Prefer document.referrer when available try { if (document.referrer) { const r = new URL(document.referrer); parentAllowed = allowedHosts.includes(r.hostname); } } catch (e) {} // Fallback: Chrome's ancestorOrigins (may be empty or absent) if (!parentAllowed && window.location.ancestorOrigins && window.location.ancestorOrigins.length) { parentAllowed = Array.from(window.location.ancestorOrigins).some((originStr) => { try { const o = new URL(originStr); return allowedHosts.includes(o.hostname); } catch (e) { return false; } }); } if (!parentAllowed) { document.body.innerHTML = '
Embedding of this calculator is only allowed on caninenutritionandwellness.com.
'; return; } } new DogCalorieCalculator(); });