2025-08-17 21:20:05 +02:00
/ * *
* Dog Calorie Calculator - iframe version
* by Canine Nutrition and Wellness
* /
class DogCalorieCalculator {
constructor ( ) {
this . currentMER = 0 ;
2025-08-18 15:33:44 +02:00
this . currentMERMin = 0 ; // For range calculations
this . currentMERMax = 0 ; // For range calculations
2025-08-17 21:20:05 +02:00
this . isImperial = false ;
2025-08-18 09:05:00 +02:00
this . theme = this . getThemeFromURL ( ) || CALCULATOR _CONFIG . defaultTheme ;
this . scale = this . getScaleFromURL ( ) || CALCULATOR _CONFIG . defaultScale ;
2025-08-17 21:20:05 +02:00
this . foodSources = [ ] ;
2025-08-18 09:05:00 +02:00
this . maxFoodSources = CALCULATOR _CONFIG . maxFoodSources ;
2025-08-18 12:45:44 +02:00
this . mealsPerDay = 2 ;
this . showPerMeal = false ;
2025-08-17 21:20:05 +02:00
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' ) ) ;
2025-08-18 09:05:00 +02:00
return ( ! isNaN ( scale ) && scale >= CALCULATOR _CONFIG . minScale && scale <= CALCULATOR _CONFIG . maxScale ) ? scale : null ;
2025-08-17 21:20:05 +02:00
}
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 ;
2025-08-18 09:05:00 +02:00
// Clamp scale between min and max for usability
const clampedScale = Math . max ( CALCULATOR _CONFIG . minScale , Math . min ( CALCULATOR _CONFIG . maxScale , this . scale ) ) ;
2025-08-17 21:20:05 +02:00
if ( clampedScale !== 1.0 ) {
container . style . transform = ` scale( ${ clampedScale } ) ` ;
container . style . transformOrigin = 'top center' ;
2025-10-28 09:58:20 +01:00
// Recalculate height for parent without adding artificial margins
2025-08-17 21:20:05 +02:00
setTimeout ( ( ) => {
this . sendHeightToParent ( ) ;
} , 100 ) ;
}
}
// 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 ,
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 ) ;
// 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 ( ) ;
}
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 = `
< div class = "dog-calculator-food-source-card" id = "foodSource-${foodSource.id}" >
< div class = "dog-calculator-food-source-header" >
< input type = "text" class = "dog-calculator-food-source-name-input" id = "food-title-${foodSource.id}" value = "${foodSource.name}" placeholder = "Enter food name" maxlength = "50" title = "Click to edit food source name" >
< button class = "dog-calculator-remove-food-btn" id = "remove-${foodSource.id}" type = "button" title = "Remove this food source" style = "display: ${this.foodSources.length > 1 ? 'block' : 'none'}" > × < / b u t t o n >
< / d i v >
< div class = "dog-calculator-input-group" >
< div class = "dog-calculator-form-group" >
< label for = "energy-${foodSource.id}" > Energy Content : < / l a b e l >
< input type = "number" id = "energy-${foodSource.id}" min = "1" step = "1" placeholder = "Enter energy content" value = "${foodSource.energy}" >
< / d i v >
< div class = "dog-calculator-form-group" >
< label for = "energy-unit-${foodSource.id}" > Unit : < / l a b e l >
< select id = "energy-unit-${foodSource.id}" class = "dog-calculator-unit-select" >
< option value = "kcal100g" $ { foodSource . energyUnit === 'kcal100g' ? 'selected' : '' } > kcal / 100 g < / o p t i o n >
< option value = "kcalkg" $ { foodSource . energyUnit === 'kcalkg' ? 'selected' : '' } > kcal / kg < / o p t i o n >
< option value = "kcalcup" $ { foodSource . energyUnit === 'kcalcup' ? 'selected' : '' } > kcal / cup < / o p t i o n >
< option value = "kcalcan" $ { foodSource . energyUnit === 'kcalcan' ? 'selected' : '' } > kcal / can < / o p t i o n >
< / s e l e c t >
< / d i v >
< / d i v >
< div id = "energy-error-${foodSource.id}" class = "dog-calculator-error dog-calculator-hidden" > Please enter a valid energy content < / d i v >
< div class = "dog-calculator-percentage-group" >
< label class = "dog-calculator-percentage-label" for = "percentage-slider-${foodSource.id}" >
Percentage of Diet : < span id = "percentage-display-${foodSource.id}" > $ { foodSource . percentage } % < / s p a n >
< span class = "dog-calculator-lock-icon unlocked" id = "lock-${foodSource.id}" title = "Lock this percentage" > 🔒 < / s p a n >
< / l a b e l >
< div class = "dog-calculator-percentage-input-group" >
< input type = "range" id = "percentage-slider-${foodSource.id}" class = "dog-calculator-percentage-slider"
min = "0" max = "100" value = "${foodSource.percentage}" >
< input type = "number" id = "percentage-input-${foodSource.id}" class = "dog-calculator-percentage-input"
min = "0" max = "100" value = "${foodSource.percentage}" >
< / d i v >
< / d i v >
< / d i v >
` ;
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 ) ;
2025-08-18 14:36:25 +02:00
// 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 ) {
const unitSelect = document . getElementById ( 'unit' ) ;
const cupsButton = document . getElementById ( 'cupsButton' ) ;
// First check if cups button will be enabled after update
const willEnableCups = this . foodSources . some ( fs =>
fs . energyUnit === 'kcalcup' && fs . energy && parseFloat ( fs . energy ) > 0
) ;
if ( willEnableCups && unitSelect ) {
// Set cups BEFORE updating calculations
unitSelect . value = 'cups' ;
unitSelect . setAttribute ( 'value' , 'cups' ) ;
this . setActiveUnitButton ( 'cups' ) ;
// Enable the cups button manually since we know it will be valid
if ( cupsButton ) {
cupsButton . disabled = false ;
cupsButton . title = 'Show amounts in cups' ;
}
}
// Now update calculations with cups already selected
this . updateFoodCalculations ( ) ;
} else {
this . updateFoodCalculations ( ) ;
}
2025-08-17 21:20:05 +02:00
} ) ;
energyInput . addEventListener ( 'blur' , ( ) => this . validateFoodSourceEnergy ( id ) ) ;
}
if ( energyUnitSelect ) {
energyUnitSelect . addEventListener ( 'change' , ( ) => {
this . updateFoodSourceData ( id , 'energyUnit' , energyUnitSelect . value ) ;
2025-08-18 14:36:25 +02:00
// 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' :
// Check if we have energy value to enable cups
const foodSource = this . foodSources . find ( fs => fs . id === id ) ;
if ( foodSource && foodSource . energy && parseFloat ( foodSource . energy ) > 0 ) {
// Set cups BEFORE updating calculations
unitSelect . value = 'cups' ;
unitSelect . setAttribute ( 'value' , 'cups' ) ;
this . setActiveUnitButton ( 'cups' ) ;
// Enable the cups button manually
const cupsButton = document . getElementById ( 'cupsButton' ) ;
if ( cupsButton ) {
cupsButton . disabled = false ;
cupsButton . title = 'Show amounts in cups' ;
}
}
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 (or ounces in imperial)
unitSelect . value = this . isImperial ? 'oz' : 'g' ;
this . setActiveUnitButton ( unitSelect . value ) ;
this . updateFoodCalculations ( ) ;
break ;
}
} else {
// No unit select, just update calculations
this . updateFoodCalculations ( ) ;
}
2025-08-17 21:20:05 +02:00
} ) ;
}
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 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 ( ) ) ;
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 ( ) ) ;
2025-08-18 12:45:44 +02:00
// 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 ( ) ;
}
}
} ) ;
}
2025-08-17 21:20:05 +02:00
// 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 ( ) ) ;
2025-10-28 09:58:20 +01:00
// Embed copy buttons removed (embedding disabled)
2025-08-17 21:20:05 +02:00
// Close modals on outside click
const shareModal = document . getElementById ( 'shareModal' ) ;
if ( shareModal ) {
shareModal . addEventListener ( 'click' , ( e ) => {
if ( e . target === shareModal ) this . hideShareModal ( ) ;
} ) ;
}
2025-10-28 09:58:20 +01:00
// Embed modal removed
2025-08-17 21:20:05 +02:00
}
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 ) ;
}
if ( this . isImperial ) {
if ( weightLabel ) weightLabel . textContent = "Dog's Weight (lbs):" ;
if ( weightInput ) {
weightInput . placeholder = "Enter weight in lbs" ;
weightInput . min = "0.2" ;
weightInput . step = "0.1" ;
}
if ( unitSelect ) {
unitSelect . innerHTML = '<option value="oz">ounces (oz)</option>' +
'<option value="lb">pounds (lb)</option>' +
'<option value="g">grams (g)</option>' +
'<option value="kg">kilograms (kg)</option>' ;
unitSelect . value = 'oz' ; // Auto-select ounces for imperial
this . setActiveUnitButton ( 'oz' ) ; // Sync unit buttons
}
// 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 ) {
weightInput . placeholder = "Enter weight in kg" ;
weightInput . min = "0.1" ;
weightInput . step = "0.1" ;
}
if ( unitSelect ) {
unitSelect . innerHTML = '<option value="g">grams (g)</option>' +
'<option value="kg">kilograms (kg)</option>' +
'<option value="oz">ounces (oz)</option>' +
'<option value="lb">pounds (lb)</option>' ;
unitSelect . value = 'g' ; // Auto-select grams for metric
this . setActiveUnitButton ( 'g' ) ; // Sync unit buttons
}
// 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' ;
}
}
} ) ;
}
}
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 ;
}
2025-08-18 15:33:44 +02:00
// 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 } ;
}
2025-08-17 21:20:05 +02:00
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' ) ;
}
}
}
2025-08-18 14:36:25 +02:00
convertUnits ( grams , unit , foodSource = null ) {
2025-08-17 21:20:05 +02:00
switch ( unit ) {
case 'kg' :
return grams / 1000 ;
case 'oz' :
return grams / 28.3495 ;
case 'lb' :
return grams / 453.592 ;
2025-08-18 14:36:25 +02:00
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
2025-08-17 21:20:05 +02:00
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' ) ;
2025-08-18 12:45:44 +02:00
const mealNote = document . getElementById ( 'mealNote' ) ;
2025-08-17 21:20:05 +02:00
if ( dayLabel && days ) {
const numDays = parseInt ( days ) ;
dayLabel . textContent = numDays === 1 ? 'day' : 'days' ;
}
2025-08-18 12:45:44 +02:00
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' ;
}
}
2025-08-17 21:20:05 +02:00
}
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 ( ) {
const dogTypeSelect = document . getElementById ( 'dogType' ) ;
const calorieResults = document . getElementById ( 'calorieResults' ) ;
const rerValue = document . getElementById ( 'rerValue' ) ;
const merValue = document . getElementById ( 'merValue' ) ;
if ( ! dogTypeSelect || ! calorieResults || ! rerValue || ! merValue ) {
return ;
}
const weightKg = this . getWeightInKg ( ) ;
const dogTypeFactor = dogTypeSelect . value ;
this . showError ( 'weightError' , false ) ;
if ( ! weightKg || weightKg < 0.1 ) {
const weightInput = document . getElementById ( 'weight' ) ;
if ( weightInput && weightInput . value ) this . showError ( 'weightError' , true ) ;
calorieResults . style . display = 'none' ;
return ;
}
if ( ! dogTypeFactor ) {
calorieResults . style . display = 'none' ;
return ;
}
const factor = parseFloat ( dogTypeFactor ) ;
const rer = this . calculateRER ( weightKg ) ;
const mer = this . calculateMER ( rer , factor ) ;
2025-08-18 15:33:44 +02:00
// Calculate range for MER
const range = this . getLifeStageRange ( factor ) ;
this . currentMERMin = this . calculateMER ( rer , range . min ) ;
this . currentMERMax = this . calculateMER ( rer , range . max ) ;
this . currentMER = mer ; // Keep middle/selected value for compatibility
2025-08-17 21:20:05 +02:00
rerValue . textContent = this . formatNumber ( rer , 0 ) + ' cal/day' ;
2025-08-18 15:33:44 +02:00
// Show MER as range if applicable
if ( range . min !== range . max ) {
merValue . textContent = this . formatNumber ( this . currentMERMin , 0 ) + '-' +
this . formatNumber ( this . currentMERMax , 0 ) + ' cal/day' ;
} else {
merValue . textContent = this . formatNumber ( mer , 0 ) + ' cal/day' ;
}
2025-08-17 21:20:05 +02:00
calorieResults . style . display = 'block' ;
this . updateFoodCalculations ( ) ;
}
2025-08-18 14:36:25 +02:00
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' ) ;
}
}
}
2025-08-17 21:20:05 +02:00
updateFoodCalculations ( ) {
if ( this . currentMER === 0 ) return ;
2025-08-18 15:33:44 +02:00
// Check if we have a range
const hasRange = this . currentMERMin !== this . currentMERMax ;
2025-08-17 21:20:05 +02:00
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' ) ;
2025-08-18 12:45:44 +02:00
const feedingConfig = document . getElementById ( 'feedingConfig' ) ;
2025-08-18 14:36:25 +02:00
// Update cups button state
this . updateCupsButtonState ( ) ;
2025-08-17 21:20:05 +02:00
if ( ! daysInput || ! unitSelect || ! dailyFoodResults || ! dailyFoodValue || ! foodAmountsSection ) {
return ;
}
const days = daysInput . value ;
2025-08-18 14:36:25 +02:00
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 ) ;
2025-08-17 21:20:05 +02:00
2025-08-18 12:45:44 +02:00
// Determine frequency suffix for display
const frequencySuffix = this . showPerMeal ? '/meal' : '/day' ;
2025-08-17 21:20:05 +02:00
// 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' ;
2025-08-18 12:45:44 +02:00
if ( feedingConfig ) feedingConfig . style . display = 'none' ;
2025-08-17 21:20:05 +02:00
// 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 ;
2025-08-18 15:33:44 +02:00
// Calculate range values if applicable
const dailyCaloriesMin = hasRange ? ( this . currentMERMin * fs . percentage ) / 100 : dailyCaloriesForThisFood ;
const dailyCaloriesMax = hasRange ? ( this . currentMERMax * fs . percentage ) / 100 : dailyCaloriesForThisFood ;
2025-08-18 14:36:25 +02:00
let dailyGramsForThisFood ;
2025-08-18 15:33:44 +02:00
let dailyGramsMin , dailyGramsMax ;
2025-08-18 14:36:25 +02:00
let dailyCupsForThisFood = null ;
2025-08-18 15:33:44 +02:00
let dailyCupsMin , dailyCupsMax ;
2025-08-18 14:36:25 +02:00
// For kcal/cup, calculate cups directly from calories
if ( fs . energyUnit === 'kcalcup' && fs . energy ) {
const caloriesPerCup = parseFloat ( fs . energy ) ;
dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup ;
2025-08-18 15:33:44 +02:00
dailyCupsMin = dailyCaloriesMin / caloriesPerCup ;
dailyCupsMax = dailyCaloriesMax / caloriesPerCup ;
2025-08-18 14:36:25 +02:00
// We still need grams for total calculation, use approximation
dailyGramsForThisFood = ( dailyCaloriesForThisFood / energyPer100g ) * 100 ;
2025-08-18 15:33:44 +02:00
dailyGramsMin = ( dailyCaloriesMin / energyPer100g ) * 100 ;
dailyGramsMax = ( dailyCaloriesMax / energyPer100g ) * 100 ;
2025-08-18 14:36:25 +02:00
} else {
// For other units, calculate grams normally
dailyGramsForThisFood = ( dailyCaloriesForThisFood / energyPer100g ) * 100 ;
2025-08-18 15:33:44 +02:00
dailyGramsMin = ( dailyCaloriesMin / energyPer100g ) * 100 ;
dailyGramsMax = ( dailyCaloriesMax / energyPer100g ) * 100 ;
2025-08-18 14:36:25 +02:00
}
2025-08-17 21:20:05 +02:00
2025-08-18 12:45:44 +02:00
// Calculate per-meal amounts if needed
const displayGrams = this . showPerMeal ? dailyGramsForThisFood / this . mealsPerDay : dailyGramsForThisFood ;
2025-08-18 15:33:44 +02:00
const displayGramsMin = this . showPerMeal ? dailyGramsMin / this . mealsPerDay : dailyGramsMin ;
const displayGramsMax = this . showPerMeal ? dailyGramsMax / this . mealsPerDay : dailyGramsMax ;
2025-08-18 14:36:25 +02:00
const displayCups = dailyCupsForThisFood !== null ?
( this . showPerMeal ? dailyCupsForThisFood / this . mealsPerDay : dailyCupsForThisFood ) : null ;
2025-08-18 15:33:44 +02:00
const displayCupsMin = dailyCupsMin !== undefined ?
( this . showPerMeal ? dailyCupsMin / this . mealsPerDay : dailyCupsMin ) : null ;
const displayCupsMax = dailyCupsMax !== undefined ?
( this . showPerMeal ? dailyCupsMax / this . mealsPerDay : dailyCupsMax ) : null ;
2025-08-18 12:45:44 +02:00
const displayCalories = this . showPerMeal ? dailyCaloriesForThisFood / this . mealsPerDay : dailyCaloriesForThisFood ;
2025-08-18 15:33:44 +02:00
const displayCaloriesMin = this . showPerMeal ? dailyCaloriesMin / this . mealsPerDay : dailyCaloriesMin ;
const displayCaloriesMax = this . showPerMeal ? dailyCaloriesMax / this . mealsPerDay : dailyCaloriesMax ;
2025-08-18 12:45:44 +02:00
2025-08-17 21:20:05 +02:00
foodBreakdowns . push ( {
name : fs . name ,
percentage : fs . percentage ,
dailyGrams : dailyGramsForThisFood ,
2025-08-18 15:33:44 +02:00
dailyGramsMin : dailyGramsMin ,
dailyGramsMax : dailyGramsMax ,
2025-08-18 12:45:44 +02:00
displayGrams : displayGrams ,
2025-08-18 15:33:44 +02:00
displayGramsMin : displayGramsMin ,
displayGramsMax : displayGramsMax ,
2025-08-18 14:36:25 +02:00
dailyCups : dailyCupsForThisFood ,
2025-08-18 15:33:44 +02:00
dailyCupsMin : dailyCupsMin ,
dailyCupsMax : dailyCupsMax ,
2025-08-18 14:36:25 +02:00
displayCups : displayCups ,
2025-08-18 15:33:44 +02:00
displayCupsMin : displayCupsMin ,
displayCupsMax : displayCupsMax ,
2025-08-17 21:20:05 +02:00
calories : dailyCaloriesForThisFood ,
2025-08-18 12:45:44 +02:00
displayCalories : displayCalories ,
2025-08-18 15:33:44 +02:00
displayCaloriesMin : displayCaloriesMin ,
displayCaloriesMax : displayCaloriesMax ,
2025-08-17 21:20:05 +02:00
isLocked : fs . isLocked ,
2025-08-18 14:36:25 +02:00
hasEnergyContent : true ,
2025-08-18 15:33:44 +02:00
hasRange : hasRange ,
2025-08-18 14:36:25 +02:00
foodSource : fs // Store reference for cups conversion
2025-08-17 21:20:05 +02:00
} ) ;
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 ,
2025-08-18 12:45:44 +02:00
displayGrams : 0 ,
2025-08-18 14:36:25 +02:00
dailyCups : null ,
displayCups : null ,
2025-08-17 21:20:05 +02:00
calories : 0 ,
2025-08-18 12:45:44 +02:00
displayCalories : 0 ,
2025-08-17 21:20:05 +02:00
isLocked : fs . isLocked ,
2025-08-18 14:36:25 +02:00
hasEnergyContent : false ,
foodSource : fs // Store reference for cups conversion
2025-08-17 21:20:05 +02:00
} ) ;
}
} ) ;
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' ;
2025-08-18 12:45:44 +02:00
if ( feedingConfig ) feedingConfig . style . display = 'none' ;
2025-08-17 21:20:05 +02:00
// 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 ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '' ;
return `
< div class = "dog-calculator-food-amount-item" >
< div class = "dog-calculator-food-amount-label" >
< span > $ { breakdown . name } < / s p a n >
< span class = "dog-calculator-food-percentage" > $ { breakdown . percentage } % < / s p a n >
$ { lockIndicator }
< / d i v >
< div class = "dog-calculator-food-amount-value dog-calculator-warning" title = "Enter energy content to calculate amount" >
⚠ ️
< / d i v >
< / d i v >
` ;
} ) . 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' ;
2025-08-18 12:45:44 +02:00
// Show feeding configuration when we have valid foods
2025-08-18 14:36:25 +02:00
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 ;
}
}
2025-08-18 12:45:44 +02:00
2025-08-17 21:20:05 +02:00
// 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 => {
2025-08-18 14:36:25 +02:00
let valueContent ;
if ( breakdown . hasEnergyContent ) {
if ( unit === 'cups' ) {
// For cups, use the pre-calculated cups value if available
if ( breakdown . displayCups !== null ) {
2025-08-18 15:33:44 +02:00
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 } ` ;
}
2025-08-18 14:36:25 +02:00
} else {
valueContent = ` <span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span> ` ;
}
} else {
2025-08-18 15:33:44 +02:00
// 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 } ` ;
}
2025-08-18 14:36:25 +02:00
}
} else {
valueContent = ` <span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span> ` ;
}
2025-08-17 21:20:05 +02:00
return `
< div class = "dog-calculator-food-result-item" >
< span class = "dog-calculator-food-result-label" > $ { breakdown . name } ( $ { breakdown . percentage } % $ { breakdown . isLocked ? ' - locked' : '' } ) : < / s p a n >
< span class = "dog-calculator-food-result-value" > $ { valueContent } < / s p a n >
< / d i v >
` ;
} ) . 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
2025-08-18 12:45:44 +02:00
const displayTotal = this . showPerMeal ? totalDailyGrams / this . mealsPerDay : totalDailyGrams ;
2025-08-18 14:36:25 +02:00
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 ;
2025-08-18 15:33:44 +02:00
let totalCupsMin = 0 ;
let totalCupsMax = 0 ;
2025-08-18 14:36:25 +02:00
foodBreakdowns . forEach ( breakdown => {
if ( breakdown . percentage > 0 && breakdown . displayCups !== null ) {
totalCups += breakdown . displayCups ;
2025-08-18 15:33:44 +02:00
if ( breakdown . hasRange ) {
totalCupsMin += breakdown . displayCupsMin || breakdown . displayCups ;
totalCupsMax += breakdown . displayCupsMax || breakdown . displayCups ;
} else {
totalCupsMin += breakdown . displayCups ;
totalCupsMax += breakdown . displayCups ;
}
2025-08-18 14:36:25 +02:00
}
} ) ;
2025-08-18 15:33:44 +02:00
if ( hasRange && totalCupsMin !== totalCupsMax ) {
totalDisplayText = ` ${ this . formatNumber ( totalCupsMin , decimals ) } - ${ this . formatNumber ( totalCupsMax , decimals ) } ${ unitLabel } ${ frequencySuffix } ` ;
} else {
totalDisplayText = this . formatNumber ( totalCups , decimals ) + ` ${ unitLabel } ${ frequencySuffix } ` ;
}
2025-08-18 14:36:25 +02:00
} else {
totalDisplayText = 'Mixed units - see breakdown' ;
}
} else {
2025-08-18 15:33:44 +02:00
// 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 } ` ;
}
2025-08-18 14:36:25 +02:00
}
dailyFoodValue . textContent = totalDisplayText ;
2025-08-17 21:20:05 +02:00
// Build HTML for individual food amounts
const foodAmountsHTML = foodBreakdowns . map ( breakdown => {
const lockIndicator = breakdown . isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '' ;
if ( ! breakdown . hasEnergyContent ) {
// Show warning for food sources without energy content
return `
< div class = "dog-calculator-food-amount-item" >
< div class = "dog-calculator-food-amount-label" >
< span > $ { breakdown . name } < / s p a n >
< span class = "dog-calculator-food-percentage" > $ { breakdown . percentage } % < / s p a n >
$ { lockIndicator }
< / d i v >
< div class = "dog-calculator-food-amount-value dog-calculator-warning" title = "Enter energy content to calculate amount" >
⚠ ️
< / d i v >
< / d i v >
` ;
} else {
2025-08-18 12:45:44 +02:00
// For multi-day calculations: show total amount for all days
2025-08-18 14:36:25 +02:00
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 = ` <span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span> ` ;
}
} 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 } ` ;
}
2025-08-17 21:20:05 +02:00
return `
< div class = "dog-calculator-food-amount-item" >
< div class = "dog-calculator-food-amount-label" >
< span > $ { breakdown . name } < / s p a n >
< span class = "dog-calculator-food-percentage" > $ { breakdown . percentage } % < / s p a n >
$ { lockIndicator }
< / d i v >
< div class = "dog-calculator-food-amount-value" >
2025-08-18 14:36:25 +02:00
$ { amountDisplay }
2025-08-17 21:20:05 +02:00
< / d i v >
< / d i v >
` ;
}
} ) . join ( '' ) ;
// Calculate and display total
const totalFoodGrams = totalDailyGrams * numDays ;
// Update the display
if ( foodAmountsList ) {
foodAmountsList . innerHTML = foodAmountsHTML ;
}
if ( totalAmountDisplay ) {
2025-08-18 14:36:25 +02:00
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 } ` ;
}
2025-08-17 21:20:05 +02:00
}
2025-10-28 09:58:20 +01:00
foodAmountsSection . style . display = 'block' ;
this . sendHeightToParent ( ) ;
2025-08-17 21:20:05 +02:00
}
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 ;
}
}
2025-10-28 09:58:20 +01:00
// Iframe auto-resize for allowed embeddings
2025-08-17 21:20:05 +02:00
setupIframeResize ( ) {
2025-10-28 09:58:20 +01:00
// Only when embedded in an iframe
if ( window . top === window . self ) return ;
2025-08-17 21:20:05 +02:00
2025-10-28 09:58:20 +01:00
// Initial send once UI is ready
setTimeout ( ( ) => this . sendHeightToParent ( ) , 50 ) ;
// Monitor for content/attribute changes
2025-08-17 21:20:05 +02:00
const observer = new MutationObserver ( ( ) => {
2025-10-28 09:58:20 +01:00
clearTimeout ( this . _resizeTimer ) ;
this . _resizeTimer = setTimeout ( ( ) => this . sendHeightToParent ( ) , 100 ) ;
2025-08-17 21:20:05 +02:00
} ) ;
observer . observe ( document . body , {
childList : true ,
subtree : true ,
attributes : true
} ) ;
2025-10-28 09:58:20 +01:00
// On viewport resize
2025-08-17 21:20:05 +02:00
window . addEventListener ( 'resize' , ( ) => this . sendHeightToParent ( ) ) ;
}
sendHeightToParent ( ) {
2025-10-28 09:58:20 +01:00
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 ) ;
2025-08-17 21:20:05 +02:00
}
2025-10-28 09:58:20 +01:00
window . parent . postMessage ( {
type : 'dogCalculatorResize' ,
height : height
} , '*' ) ;
2025-08-17 21:20:05 +02:00
}
// Modal functionality
showShareModal ( ) {
const modal = document . getElementById ( 'shareModal' ) ;
const shareUrl = document . getElementById ( 'shareUrl' ) ;
if ( modal && shareUrl ) {
shareUrl . value = window . location . href ;
2025-10-28 09:58:20 +01:00
// 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 ( ) ;
2025-08-17 21:20:05 +02:00
}
}
hideShareModal ( ) {
const modal = document . getElementById ( 'shareModal' ) ;
if ( modal ) modal . style . display = 'none' ;
2025-10-28 09:58:20 +01:00
this . sendHeightToParent ( ) ;
2025-08-17 21:20:05 +02:00
}
2025-10-28 09:58:20 +01:00
// Embed modal removed (embedding disabled)
2025-08-17 21:20:05 +02:00
2025-10-28 09:58:20 +01:00
// Embed modal removed (embedding disabled)
2025-08-17 21:20:05 +02:00
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' ) ;
}
}
}
2025-10-28 09:58:20 +01:00
// Embed code copy removed (embedding disabled)
2025-08-17 21:20:05 +02:00
}
// Initialize calculator when DOM is ready
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
2025-10-28 09:58:20 +01:00
// 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 = '<div style="max-width:720px;margin:40px auto;padding:16px;border:1px solid #ddd;border-radius:8px;font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif;color:#333;line-height:1.5;text-align:center;">Embedding of this calculator is only allowed on caninenutritionandwellness.com.</div>' ;
return ;
}
}
2025-08-17 21:20:05 +02:00
new DogCalorieCalculator ( ) ;
} ) ;