5 Commits

Author SHA1 Message Date
Dayowe 90657f9aa4 Remove embedding and js widget 2025-10-28 09:58:20 +01:00
Dayowe 99b516d087 Add range calculations for dog food amounts based on life stage
- Implement range multipliers for different life stages (e.g., 1.6-1.8x for intact adults)
- Display MER and food amounts as ranges (e.g., '2042-2298 cal/day')
- Add CSS to prevent value wrapping with white-space: nowrap
- Increase calculator max-width from 600px to 640px for better text layout
- Based on veterinary RER multiplier ranges for more accurate feeding recommendations
2025-08-18 15:33:44 +02:00
Dayowe c4770d5ee6 Fix data-theme and data-scale widget attributes not being applied
- Remove theme/scale assignments that override widget options in build process
- Widget now properly uses data-theme and data-scale attributes from HTML
- Both light and dark themes work correctly when multiple widgets on same page
- Scale attribute properly applies to each widget independently
2025-08-18 14:54:25 +02:00
Dayowe 4ec42fdbc0 Add cups unit option for kcal/cup measurements
Features:
- Add cups as a unit option that only works with kcal/cup energy measurements
- Cups button is disabled with tooltip when kcal/cup is not selected
- Auto-select cups when user selects kcal/cup as energy unit
- Auto-switch to appropriate units when changing energy measurement types
- Calculate cup amounts directly from calories without density assumptions

Fixes:
- Ensure cups display correct values immediately upon auto-selection
- Fix "Per day" radio button to be pre-selected when feeding config becomes visible
- Handle empty unit values with failsafe fallback to active button

Technical details:
- Direct calorie-to-cups conversion bypassing gram conversions
- Pre-calculate cups values in food breakdown for efficient display
- Set unit before updating calculations to avoid timing issues
- Added CSS styling for disabled button state across all themes
2025-08-18 14:36:25 +02:00
Dayowe 8758ac1dbc Add per-meal feeding frequency feature
- Added feeding configuration section with daily/per-meal toggle
- Implemented meals per day selector (1-10 meals)
- Updated all calculations to support per-meal division
- Dynamic unit labels change from "/day" to "/meal"
- Added helpful meal count display for multi-day calculations
- Proper dark theme support for new UI elements
- Mobile responsive design for feeding controls
- Centered meal input group for better UX

This feature allows users to easily switch between viewing daily food amounts
and per-meal portions, making feeding schedules more practical to follow.
2025-08-18 12:45:44 +02:00
9 changed files with 1655 additions and 4252 deletions
+41 -110
View File
@@ -1,6 +1,6 @@
# 🐕 Sundog Dog Food Calorie Calculator
A professional veterinary nutrition tool for calculating dogs' daily calorie requirements and food amounts. Features advanced multi-food source management, percentage locking, and detailed food amount breakdowns. Built for embedding on websites with complete brand protection options.
A professional veterinary nutrition tool for calculating dogs' daily calorie requirements and food amounts. Features advanced multi-food source management, percentage locking, and detailed food amount breakdowns. Distributed as a standalone page; thirdparty embedding is no longer supported.
**By [Canine Nutrition and Wellness](https://caninenutritionandwellness.com)**
@@ -28,10 +28,9 @@ A professional veterinary nutrition tool for calculating dogs' daily calorie req
- **Lock Indicators**: Visual indicators showing which percentages are locked
### User Experience
- **Scalable Widget**: Easily resize from 50% to 200% with data attributes
- **Scalable UI**: Resize from 50% to 200% via query params
- **Theme Support**: Light, dark, and system themes
- **Responsive Design**: Mobile-first, optimized layouts for all devices
- **Two Embedding Options**: JavaScript widget and iframe
- **Accessibility**: Full keyboard navigation and screen reader support
### Brand & Integration
@@ -39,36 +38,16 @@ A professional veterinary nutrition tool for calculating dogs' daily calorie req
- **Professional Design**: Clean, veterinary-grade interface
- **Brand Protection**: Complete iframe isolation option
## 🚀 Quick Start
## 🚀 Usage
- Open `iframe.html` locally, or host it as a standalone page on your site.
- Embedding is allowed only on these domains: `caninenutritionandwellness.com`, `www.caninenutritionandwellness.com`.
- Use an iframe to embed on your site, for example:
### Option 1: JavaScript Widget (Recommended)
```html
<!-- Basic usage -->
<script src="https://yourdomain.com/sundog-dog-food-calculator.js"></script>
<div id="dog-calorie-calculator"></div>
<!-- With theme and scale options -->
<div id="dog-calorie-calculator" data-theme="dark" data-scale="0.8"></div>
```
### Option 2: iframe Embed
```html
<!-- Basic iframe -->
<iframe
src="https://yourdomain.com/iframe.html"
width="100%"
height="600"
frameborder="0"
title="Dog Calorie Calculator">
</iframe>
<!-- With theme and scale parameters -->
<iframe
src="https://yourdomain.com/iframe.html?theme=dark&scale=1.2"
width="100%"
height="600"
frameborder="0"
title="Dog Calorie Calculator">
src="https://embed.caninenutritionandwellness.com/dog-calorie-calculator/iframe.html?theme=light&scale=0.8"
width="100%" height="640" frameborder="0" title="Dog Calorie Calculator">
</iframe>
```
@@ -134,7 +113,7 @@ Choose from three themes:
- `system` - Follows user's OS preference (default)
### Scale Options
Resize the widget from 50% to 200%:
Resize the interface from 50% to 200%:
- Range: `0.5` to `2.0`
- Default: `1.0` (100% size)
- Examples: `0.8` (80%), `1.2` (120%), `1.5` (150%)
@@ -146,27 +125,21 @@ Support for regional differences:
- `kcal/cup` - US/Canada dry food
- `kcal/can` - US/Canada wet food
### Advanced JavaScript Usage
```javascript
new DogCalorieCalculatorWidget(container, {
theme: 'dark', // 'light', 'dark', 'system'
scale: 1.2 // 0.5 to 2.0
});
```
<!-- Embedding and external widget initialization are no longer supported. -->
## 🛠️ Development
### Build System
This project uses a single source of truth approach:
This project uses an organized source layout compiled into a single page:
- **Master Source**: `iframe.html` - Contains all functionality, styles, and calculations
- **Build Script**: `build.js` - Generates the widget from iframe.html
- **Generated Output**: `sundog-dog-food-calculator.js` - Embeddable widget
- **Sources**: `src/` (HTML, CSS, JS modules)
- **Build Script**: `build.js` - Generates `iframe.html` from `src/`
- **Output**: `iframe.html` - Standalone calculator page
### Development Workflow
1. **Make changes to `iframe.html`** - Edit calculations, design, layout, or functionality
1. **Make changes in `src/`** - Edit calculations, design, layout, or functionality
2. **Run the build script**: `node build.js`
3. **Done!** - Both iframe and widget now have identical functionality
3. **Done!** - `iframe.html` is regenerated
### Why This Approach?
-**Single Source of Truth** - No need to maintain two separate files
@@ -175,18 +148,16 @@ This project uses a single source of truth approach:
-**No Sync Issues** - Build script ensures consistency
### Build Script Features
- Extracts CSS, HTML, and JavaScript from iframe.html
- Transforms CSS classes for widget namespacing (`dog-calculator-``dog-calc-`)
- Compiles CSS, HTML, and JavaScript from `src/`
- Preserves all functionality including unit switching and calculations
- Maintains theme and scale support via data attributes
- Maintains theme and scale support via URL query parameters
## 📁 Project Structure
```
├── iframe.html # 🎯 MASTER SOURCE - Edit this file
├── src/ # ✏️ Source (HTML/CSS/JS)
├── build.js # 🔧 Build script - Run after changes
├── sundog-dog-food-calculator.js # 📦 Generated widget (don't edit)
├── test-widget.html # 🧪 Test file for widget
├── iframe.html # 📦 Generated standalone page
└── README.md # 📖 This file
```
@@ -217,71 +188,32 @@ Colors automatically adapt to light/dark themes via CSS custom properties.
| Working dog - heavy work | 5.0 | Intensive work |
| Senior dog | 1.1 | Reduced activity |
## 🔧 Technical Implementation
## 🔧 Technical Notes
- Standalone page with theme and scale controls via URL params.
- Embedding is allowlisted at runtime to your domains and should be enforced with server headers.
### JavaScript Widget Features
- **Auto-initialization**: Detects `#dog-calorie-calculator` containers
- **CSS Namespacing**: All classes prefixed with `dog-calc-`
- **Shadow DOM Ready**: Prepared for better style isolation
- **Real-time Validation**: Input validation with error messages
- **Mobile Optimized**: Responsive breakpoints and touch-friendly
### Server Headers (required for robust enforcement)
Configure your server or CDN to send this header on `iframe.html`:
### iframe Features
- **Auto-resize**: Communicates height changes to parent
- **Style Isolation**: Complete protection from host site CSS
- **Loading Animation**: Smooth fade-in when ready
- **Cross-origin Ready**: PostMessage communication for integration
```
Content-Security-Policy: frame-ancestors https://caninenutritionandwellness.com https://www.caninenutritionandwellness.com;
```
Optional legacy header (deprecated but harmless as a supplement):
```
X-Frame-Options: SAMEORIGIN
```
If you serve the calculator from a subdomain (e.g., `embed.caninenutritionandwellness.com`) and embed it on the root domain, prefer the CSP `frame-ancestors` directive above.
## 🚀 Deployment Guide
### 1. Host the Files
Upload these files to your web server:
- `sundog-dog-food-calculator.js` (for widget embedding)
- `iframe.html` (for iframe embedding)
### 2. Update URLs
Replace `https://yourdomain.com` in:
- `test-widget.html` examples
- `sundog-dog-food-calculator.js` comments
- This README
### 3. CDN Distribution (Optional)
For better performance, serve the widget script via CDN:
- Use CloudFlare, AWS CloudFront, or similar
- Enable CORS headers for cross-origin requests
- Set appropriate cache headers (1 day for updates)
### 4. Analytics Integration
Add tracking to understand usage:
```javascript
// Track widget interactions
document.addEventListener('DOMContentLoaded', function() {
// Track when calculator is used
document.addEventListener('change', function(e) {
if (e.target.closest('.dog-calc-widget')) {
gtag('event', 'calculator_interaction', {
'event_category': 'dog_calculator',
'event_label': e.target.id
});
}
});
});
```
### Deployment
Host `iframe.html` (e.g., on `embed.caninenutritionandwellness.com`) and embed via iframe on your approved domains.
## 🔒 Brand Protection
### JavaScript Widget Risks
Users can override your styling with:
```css
.dog-calc-footer { display: none !important; }
```
### iframe Protection
Your branding is completely protected in iframe mode. Users cannot:
- Remove your footer link
- Modify your styling
- Access your content with JavaScript
Embedding is disabled to protect branding and ensure consistent presentation.
## 📱 Mobile Optimization
@@ -353,9 +285,8 @@ This calculator is provided for educational and professional use. The formulas a
## 🔗 Links
- **Website**: [caninenutritionandwellness.com](https://caninenutritionandwellness.com)
- **Widget Demo**: Open `test-widget.html` in your browser
- **Standalone**: Open `iframe.html` in your browser
---
**Built with ❤️ for canine nutrition professionals**
**Built with ❤️ for canine nutrition professionals**
+12 -16
View File
@@ -3,8 +3,8 @@
/**
* Dog Calculator Build System - ORGANIZED VERSION
*
* This build script generates iframe.html and sundog-dog-food-calculator.js
* from organized source files in the src/ directory.
* This build script generates iframe.html from organized source files
* in the src/ directory.
*
* Source structure:
* - src/index.html - HTML template
@@ -15,7 +15,6 @@
*
* Output files:
* - iframe.html - Standalone calculator page
* - sundog-dog-food-calculator.js - Embeddable widget
*
* Usage: node build.js
*/
@@ -85,7 +84,7 @@ function backupFiles() {
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filesToBackup = ['iframe.html', 'sundog-dog-food-calculator.js'];
const filesToBackup = ['iframe.html'];
filesToBackup.forEach(file => {
if (fs.existsSync(file)) {
@@ -141,8 +140,9 @@ function createWidgetJS(css, html, js) {
.replace(/document\.querySelectorAll\(/g, 'this.container.querySelectorAll(')
// Remove the DOMContentLoaded listener and class instantiation - we'll handle this in the widget wrapper
.replace(/document\.addEventListener\('DOMContentLoaded'.*?\n.*?new DogCalorieCalculator.*?\n.*?\}\);/s, '')
// Remove duplicate theme/scale assignments that override options
.replace(/this\.theme = this\.getThemeFromURL\(\) \|\| 'system';\s*\n\s*this\.scale = this\.getScaleFromURL\(\) \|\| 1\.0;/g, '')
// Remove theme/scale assignments that would override widget options
.replace(/this\.theme = this\.getThemeFromURL\(\) \|\| CALCULATOR_CONFIG\.defaultTheme;/g, '')
.replace(/this\.scale = this\.getScaleFromURL\(\) \|\| CALCULATOR_CONFIG\.defaultScale;/g, '')
// Add widget initialization methods
.replace(/constructor\(\) \{/, `constructor(container, options = {}) {
this.container = container;
@@ -186,14 +186,14 @@ function createWidgetJS(css, html, js) {
// Remove existing theme classes
calculatorContainer.classList.remove('theme-light', 'theme-dark', 'theme-system');
// Add the selected theme class
if (['light', 'dark', 'system'].includes(this.options.theme)) {
calculatorContainer.classList.add('theme-' + this.options.theme);
if (['light', 'dark', 'system'].includes(this.theme)) {
calculatorContainer.classList.add('theme-' + this.theme);
}
}
}
applyScale() {
const scale = Math.max(0.5, Math.min(2.0, this.options.scale));
const scale = Math.max(0.5, Math.min(2.0, this.scale));
if (scale !== 1.0) {
this.container.style.transform = \`scale(\${scale})\`;
this.container.style.transformOrigin = 'top left';
@@ -298,10 +298,7 @@ function build() {
fs.writeFileSync('iframe.html', iframeContent);
console.log(' ✅ Generated iframe.html');
// Generate widget
const widgetCode = createWidgetJS(css, html, js);
fs.writeFileSync('sundog-dog-food-calculator.js', widgetCode);
console.log(' ✅ Generated sundog-dog-food-calculator.js');
// Embeddable widget generation removed (embedding no longer supported)
console.log('');
console.log('🎉 Build completed successfully!');
@@ -316,12 +313,11 @@ function build() {
console.log('');
console.log(' Generated files:');
console.log(' • iframe.html - Standalone calculator');
console.log(' • sundog-dog-food-calculator.js - Embeddable widget');
console.log('');
console.log('🔄 Your workflow:');
console.log(' 1. Edit organized files in src/');
console.log(' 2. Run: node build.js');
console.log(' 3. Both output files are regenerated!');
console.log(' 3. Output file is regenerated!');
console.log('');
console.log('💡 Clean, organized structure - easy to maintain!');
@@ -337,4 +333,4 @@ if (require.main === module) {
build();
}
module.exports = { build };
module.exports = { build };
+801 -273
View File
File diff suppressed because it is too large Load Diff
+121 -6
View File
@@ -15,14 +15,14 @@
margin: 0;
padding: 0;
background: transparent;
overflow-x: hidden;
overflow: hidden; /* hide internal scrollbars; parent resizes iframe */
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.5;
color: var(--text-primary);
}
.dog-calculator-container {
max-width: 600px;
max-width: 640px;
margin: 0 auto;
padding: 24px;
box-sizing: border-box;
@@ -203,6 +203,7 @@
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
gap: 10px; /* Add gap between label and value */
}
.dog-calculator-result-item:last-child {
@@ -222,6 +223,7 @@
padding: 4px 12px;
background: rgba(241, 154, 95, 0.15);
border-radius: 4px;
white-space: nowrap; /* Prevent text from wrapping to multiple lines */
}
.dog-calculator-collapsible {
@@ -319,10 +321,7 @@
color: #9f5999;
}
.dog-calculator-btn-embed:hover {
border-color: var(--success-color);
color: var(--success-color);
}
/* Embed button removed */
.dog-calculator-footer {
text-align: center;
@@ -437,4 +436,120 @@
}
}
/* Feeding Configuration Styles */
.dog-calculator-container .dog-calculator-feeding-config {
margin-top: 20px;
padding: 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.dog-calculator-container .dog-calculator-frequency-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.dog-calculator-container .dog-calculator-frequency-row > label {
font-weight: 500;
color: var(--text-label);
margin: 0;
}
.dog-calculator-container .dog-calculator-radio-group {
display: flex;
gap: 20px;
align-items: center;
}
.dog-calculator-container .dog-calculator-radio-group label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
color: var(--text-primary);
font-size: 0.95rem;
}
.dog-calculator-container .dog-calculator-radio-group input[type="radio"] {
cursor: pointer;
margin: 0;
}
.dog-calculator-container .dog-calculator-radio-group input[type="radio"]:checked + span {
font-weight: 600;
color: var(--accent-color);
}
.dog-calculator-container .dog-calculator-meal-input {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0 auto;
}
.dog-calculator-container .dog-calculator-meal-input span {
color: var(--text-secondary);
font-size: 0.95rem;
}
.dog-calculator-container .dog-calculator-meal-input input[type="number"] {
width: 50px;
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.95rem;
color: var(--text-primary);
background: var(--bg-secondary);
text-align: center;
}
.dog-calculator-container .dog-calculator-meal-input input[type="number"]:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(241, 154, 95, 0.1);
}
/* Update meal note styling */
.dog-calculator-container #mealNote {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: normal;
margin-left: 4px;
}
/* Mobile responsive adjustments for feeding config */
@media (max-width: 480px) {
.dog-calculator-meal-input {
margin-left: 0;
width: 100%;
margin-top: 8px;
}
.dog-calculator-frequency-row {
flex-direction: column;
align-items: flex-start;
}
/* Stack result items vertically on small screens */
.dog-calculator-result-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.dog-calculator-result-label {
margin-right: 0;
font-size: 0.9rem;
}
.dog-calculator-result-value {
font-size: 1rem;
align-self: stretch;
text-align: center;
}
}
/* Dark theme - manual override */
+135 -123
View File
@@ -1,9 +1,13 @@
.dog-calculator-container.theme-dark {
--bg-primary: #24202d;
--bg-secondary: #312b3b;
--bg-tertiary: #1f1b26;
--border-color: #433c4f;
--text-primary: #f5f3f7;
--text-secondary: #b8b0c2;
--text-label: #9f94ae;
--success-color: #7fa464;
--error-color: #e87159;
color: var(--text-primary);
}
@@ -106,6 +110,19 @@
background: #f19a5f;
color: white;
}
.dog-calculator-container.theme-dark .dog-calculator-unit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--border-color);
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.dog-calculator-container.theme-dark .dog-calculator-unit-btn:disabled:hover {
border-color: var(--border-color);
background: var(--bg-tertiary);
}
.dog-calculator-container.theme-dark .dog-calculator-results {
background: linear-gradient(135deg, rgba(241, 154, 95, 0.15) 0%, rgba(241, 154, 95, 0.08) 100%);
@@ -143,9 +160,43 @@
color: #f19a5f;
}
.dog-calculator-container.theme-dark .dog-calculator-btn-embed:hover {
border-color: var(--success-color);
color: var(--success-color);
/* Embed button removed */
/* Dark theme feeding configuration styles */
.dog-calculator-container.theme-dark .dog-calculator-feeding-config {
background: var(--bg-tertiary);
border-color: var(--border-color);
}
.dog-calculator-container.theme-dark .dog-calculator-frequency-row > label {
color: var(--text-label);
}
.dog-calculator-container.theme-dark .dog-calculator-radio-group label {
color: var(--text-primary);
}
.dog-calculator-container.theme-dark .dog-calculator-radio-group input[type="radio"]:checked + span {
color: #f19a5f;
}
.dog-calculator-container.theme-dark .dog-calculator-meal-input span {
color: var(--text-secondary);
}
.dog-calculator-container.theme-dark .dog-calculator-meal-input input[type="number"] {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
}
.dog-calculator-container.theme-dark .dog-calculator-meal-input input[type="number"]:focus {
border-color: #f19a5f;
box-shadow: 0 0 0 2px rgba(241, 154, 95, 0.15);
}
.dog-calculator-container.theme-dark #mealNote {
color: var(--text-secondary);
}
/* System theme - follows user's OS preference */
@@ -153,9 +204,13 @@
.dog-calculator-container.theme-system {
--bg-primary: #24202d;
--bg-secondary: #312b3b;
--bg-tertiary: #1f1b26;
--border-color: #433c4f;
--text-primary: #f5f3f7;
--text-secondary: #b8b0c2;
--text-label: #9f94ae;
--success-color: #7fa464;
--error-color: #e87159;
color: var(--text-primary);
}
@@ -258,6 +313,19 @@
background: #f19a5f;
color: white;
}
.dog-calculator-container.theme-system .dog-calculator-unit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--border-color);
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.dog-calculator-container.theme-system .dog-calculator-unit-btn:disabled:hover {
border-color: var(--border-color);
background: var(--bg-tertiary);
}
.dog-calculator-container.theme-system .dog-calculator-results {
background: linear-gradient(135deg, rgba(241, 154, 95, 0.15) 0%, rgba(241, 154, 95, 0.08) 100%);
@@ -295,21 +363,60 @@
color: #f19a5f;
}
.dog-calculator-container.theme-system .dog-calculator-btn-embed:hover {
border-color: var(--success-color);
color: var(--success-color);
/* Embed button removed */
/* System theme feeding configuration styles in dark mode */
.dog-calculator-container.theme-system .dog-calculator-feeding-config {
background: var(--bg-tertiary);
border-color: var(--border-color);
}
.dog-calculator-container.theme-system .dog-calculator-frequency-row > label {
color: var(--text-label);
}
.dog-calculator-container.theme-system .dog-calculator-radio-group label {
color: var(--text-primary);
}
.dog-calculator-container.theme-system .dog-calculator-radio-group input[type="radio"]:checked + span {
color: #f19a5f;
}
.dog-calculator-container.theme-system .dog-calculator-meal-input span {
color: var(--text-secondary);
}
.dog-calculator-container.theme-system .dog-calculator-meal-input input[type="number"] {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
}
.dog-calculator-container.theme-system .dog-calculator-meal-input input[type="number"]:focus {
border-color: #f19a5f;
box-shadow: 0 0 0 2px rgba(241, 154, 95, 0.15);
}
.dog-calculator-container.theme-system #mealNote {
color: var(--text-secondary);
}
}
/* Modal Styles */
.dog-calculator-modal {
display: none;
display: none; /* set to flex via JS when opened */
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
overflow: auto; /* allow modal content scroll if needed */
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
@@ -321,19 +428,19 @@
.dog-calculator-modal-content {
position: relative;
background-color: var(--bg-secondary);
margin: 5% auto;
margin: 0;
padding: 30px;
border: 1px solid var(--border-color);
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh; /* ensure it fits viewport */
overflow: auto;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease;
}
.dog-calculator-modal-embed {
max-width: 700px;
}
/* Embed modal removed */
@keyframes slideIn {
from {
@@ -419,74 +526,7 @@
color: var(--text-primary);
}
/* Embed Modal */
.dog-calculator-embed-options {
display: flex;
flex-direction: column;
gap: 24px;
}
.dog-calculator-embed-option {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
background: #fcfafd;
}
.dog-calculator-embed-option h4 {
margin: 0 0 8px 0;
color: var(--text-primary);
font-size: 1.1rem;
}
.dog-calculator-embed-option p {
margin: 0 0 16px 0;
color: var(--text-label);
font-size: 0.9rem;
}
/* Default (light theme) code containers */
.dog-calculator-code-container {
position: relative;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
}
.dog-calculator-code-container pre {
margin: 0;
padding: 16px 60px 16px 16px;
overflow-x: auto;
}
.dog-calculator-code-container code {
color: var(--text-primary);
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.4;
}
.dog-calculator-copy-btn {
position: absolute;
top: 8px;
right: 8px;
padding: 6px 10px;
background: #f19a5f;
color: white;
border: none;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
z-index: 1;
}
.dog-calculator-copy-btn:hover { background: #e87741; }
.dog-calculator-copy-btn.copied { background: var(--success-color); }
.dog-calculator-copy-btn.copied:hover { background: var(--success-color); }
/* Embed UI removed */
/* Dark theme modal styles */
.dog-calculator-container.theme-dark .dog-calculator-modal-content {
@@ -512,28 +552,7 @@
color: var(--text-primary);
}
.dog-calculator-container.theme-dark .dog-calculator-embed-option {
background: var(--bg-secondary);
border-color: var(--border-color);
}
.dog-calculator-container.theme-dark .dog-calculator-embed-option h4 {
color: var(--text-primary);
}
.dog-calculator-container.theme-dark .dog-calculator-embed-option p {
color: var(--text-secondary)
}
/* Dark theme code containers - different from embed option background */
.dog-calculator-container.theme-dark .dog-calculator-code-container {
background: #1a1621;
border-color: #2a2330;
}
.dog-calculator-container.theme-dark .dog-calculator-code-container code {
color: var(--text-primary);
}
/* Embed UI removed for dark theme */
/* System theme modal styles */
@media (prefers-color-scheme: dark) {
@@ -560,28 +579,7 @@
color: var(--text-primary);
}
.dog-calculator-container.theme-system .dog-calculator-embed-option {
background: var(--bg-secondary);
border-color: var(--border-color);
}
.dog-calculator-container.theme-system .dog-calculator-embed-option h4 {
color: var(--text-primary);
}
.dog-calculator-container.theme-system .dog-calculator-embed-option p {
color: var(--text-secondary)
}
/* System theme code containers - different from embed option background */
.dog-calculator-container.theme-system .dog-calculator-code-container {
background: #1a1621;
border-color: #2a2330;
}
.dog-calculator-container.theme-system .dog-calculator-code-container code {
color: var(--text-primary);
}
/* Embed UI removed for system theme */
}
/* Multi-Food Source Styles */
@@ -1208,6 +1206,20 @@
color: white;
}
.dog-calculator-unit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--border-color);
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.dog-calculator-unit-btn:disabled:hover {
border-color: var(--border-color);
background: var(--bg-tertiary);
transform: none;
}
/* Hidden unit select for compatibility */
.dog-calculator-unit-select-hidden {
display: none;
+26 -33
View File
@@ -65,6 +65,28 @@
<span>Add another food source</span>
</button>
<!-- Feeding Configuration -->
<div class="dog-calculator-feeding-config" id="feedingConfig" style="display: none;">
<div class="dog-calculator-frequency-row">
<label>Show amounts:</label>
<div class="dog-calculator-radio-group">
<label>
<input type="radio" name="showAs" value="daily" id="showDaily" checked>
<span>Per day</span>
</label>
<label>
<input type="radio" name="showAs" value="meal" id="showPerMeal">
<span>Per meal</span>
</label>
</div>
<div class="dog-calculator-meal-input" id="mealInputGroup" style="display: none;">
<span>×</span>
<input type="number" id="mealsPerDay" value="2" min="1" max="10">
<span>meals/day</span>
</div>
</div>
</div>
<!-- Per-Food Results -->
<div class="dog-calculator-food-results" id="foodBreakdownResults" style="display: none;">
<div id="foodBreakdownList">
@@ -78,6 +100,7 @@
<button type="button" class="dog-calculator-unit-btn" data-unit="kg">kg</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="oz">oz</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="lb">lb</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="cups" id="cupsButton" disabled title="Available when using kcal/cup measurement">cups</button>
</div>
<!-- Daily Total Results -->
@@ -94,13 +117,14 @@
<option value="kg">kilograms (kg)</option>
<option value="oz">ounces (oz)</option>
<option value="lb">pounds (lb)</option>
<option value="cups">cups</option>
</select>
<div class="dog-calculator-food-amounts-section" id="foodAmountsSection" style="display: none;">
<h4 class="dog-calculator-section-title">
Calculate amounts for
<input type="number" id="days" min="1" step="1" value="1" placeholder="1" aria-describedby="daysHelp" class="dog-calculator-inline-days">
<span id="dayLabel">day</span>:
<span id="dayLabel">day</span><span id="mealNote" style="display: none;"></span>:
</h4>
<div id="daysError" class="dog-calculator-error dog-calculator-hidden">Please enter a valid number of days (minimum 1)</div>
<div id="foodAmountsList" class="dog-calculator-food-amounts-list">
@@ -119,9 +143,6 @@
<button class="dog-calculator-btn dog-calculator-btn-share" id="shareBtn">
Share
</button>
<button class="dog-calculator-btn dog-calculator-btn-embed" id="embedBtn">
Embed
</button>
</div>
<div class="dog-calculator-footer">
@@ -157,33 +178,5 @@
</div>
</div>
<!-- Embed Modal -->
<div id="embedModal" class="dog-calculator-modal" style="display: none;">
<div class="dog-calculator-modal-content dog-calculator-modal-embed">
<span class="dog-calculator-modal-close" id="embedModalClose">&times;</span>
<h3>⚡ Embed the Calculator</h3>
<div class="dog-calculator-embed-options">
<div class="dog-calculator-embed-option">
<h4>⚡ JavaScript Widget</h4>
<div class="dog-calculator-code-container">
<pre><code id="widgetCode"></code></pre>
<button class="dog-calculator-copy-btn plausible-event-name=Calculator+Usage plausible-event-action=calculator-embed-js" id="copyWidget">
Copy
</button>
</div>
</div>
<div class="dog-calculator-embed-option">
<h4>🛡️ iframe Embed</h4>
<div class="dog-calculator-code-container">
<pre><code id="iframeCode"></code></pre>
<button class="dog-calculator-copy-btn plausible-event-name=Calculator+Usage plausible-event-action=calculator-embed-iframe" id="copyIframe">
Copy
</button>
</div>
</div>
</div>
</div>
</div>
</div>
+519 -111
View File
@@ -6,11 +6,15 @@
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;
this.init();
}
@@ -56,10 +60,8 @@
container.style.transform = `scale(${clampedScale})`;
container.style.transformOrigin = 'top center';
// Adjust container to account for scaling
// Recalculate height for parent without adding artificial margins
setTimeout(() => {
const actualHeight = container.offsetHeight * clampedScale;
container.style.marginBottom = `${(clampedScale - 1) * container.offsetHeight}px`;
this.sendHeightToParent();
}, 100);
}
@@ -587,7 +589,36 @@
if (energyInput) {
energyInput.addEventListener('input', () => {
this.updateFoodSourceData(id, 'energy', energyInput.value);
this.updateFoodCalculations();
// 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();
}
});
energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id));
}
@@ -595,7 +626,54 @@
if (energyUnitSelect) {
energyUnitSelect.addEventListener('change', () => {
this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value);
this.updateFoodCalculations();
// 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();
}
});
}
@@ -765,16 +843,64 @@
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 embedBtn = document.getElementById('embedBtn');
const shareModalClose = document.getElementById('shareModalClose');
const embedModalClose = document.getElementById('embedModalClose');
if (shareBtn) shareBtn.addEventListener('click', () => this.showShareModal());
if (embedBtn) embedBtn.addEventListener('click', () => this.showEmbedModal());
if (shareModalClose) shareModalClose.addEventListener('click', () => this.hideShareModal());
if (embedModalClose) embedModalClose.addEventListener('click', () => this.hideEmbedModal());
// Share buttons
const shareFacebook = document.getElementById('shareFacebook');
@@ -789,16 +915,10 @@
if (shareEmail) shareEmail.addEventListener('click', () => this.shareViaEmail());
if (shareCopy) shareCopy.addEventListener('click', () => this.copyShareLink());
// Copy buttons
const copyWidget = document.getElementById('copyWidget');
const copyIframe = document.getElementById('copyIframe');
if (copyWidget) copyWidget.addEventListener('click', () => this.copyEmbedCode('widget'));
if (copyIframe) copyIframe.addEventListener('click', () => this.copyEmbedCode('iframe'));
// Embed copy buttons removed (embedding disabled)
// Close modals on outside click
const shareModal = document.getElementById('shareModal');
const embedModal = document.getElementById('embedModal');
if (shareModal) {
shareModal.addEventListener('click', (e) => {
@@ -806,11 +926,7 @@
});
}
if (embedModal) {
embedModal.addEventListener('click', (e) => {
if (e.target === embedModal) this.hideEmbedModal();
});
}
// Embed modal removed
}
toggleUnits() {
@@ -923,6 +1039,25 @@
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;
@@ -941,7 +1076,7 @@
}
}
convertUnits(grams, unit) {
convertUnits(grams, unit, foodSource = null) {
switch (unit) {
case 'kg':
return grams / 1000;
@@ -949,6 +1084,18 @@
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;
}
@@ -983,10 +1130,21 @@
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) {
@@ -1031,19 +1189,57 @@
const rer = this.calculateRER(weightKg);
const mer = this.calculateMER(rer, factor);
this.currentMER = mer;
// 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
rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day';
merValue.textContent = this.formatNumber(mer, 0) + ' cal/day';
// 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';
}
calorieResults.style.display = 'block';
this.updateFoodCalculations();
this.sendHeightToParent();
}
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');
@@ -1053,15 +1249,36 @@
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;
const unit = unitSelect.value;
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb';
const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : 1;
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 => {
@@ -1075,6 +1292,7 @@
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');
@@ -1094,15 +1312,71 @@
if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) {
const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100;
const dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 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
hasEnergyContent: true,
hasRange: hasRange,
foodSource: fs // Store reference for cups conversion
});
totalDailyGrams += dailyGramsForThisFood;
@@ -1113,9 +1387,14 @@
name: fs.name,
percentage: fs.percentage,
dailyGrams: 0,
displayGrams: 0,
dailyCups: null,
displayCups: null,
calories: 0,
displayCalories: 0,
isLocked: fs.isLocked,
hasEnergyContent: false
hasEnergyContent: false,
foodSource: fs // Store reference for cups conversion
});
}
});
@@ -1131,6 +1410,7 @@
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');
@@ -1177,6 +1457,17 @@
// 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';
@@ -1184,9 +1475,32 @@
// Update per-food breakdown
if (foodBreakdownList && foodBreakdowns.length > 1) {
const breakdownHTML = foodBreakdowns.map(breakdown => {
const valueContent = breakdown.hasEnergyContent
? `${this.formatNumber(this.convertUnits(breakdown.dailyGrams, unit), decimals)} ${unitLabel}/day`
: `<span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span>`;
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 = `<span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span>`;
}
} 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 = `<span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span>`;
}
return `
<div class="dog-calculator-food-result-item">
@@ -1205,8 +1519,71 @@
// Generate individual food amount breakdown
// Update daily food value with correct units
const convertedDailyTotal = this.convertUnits(totalDailyGrams, unit);
dailyFoodValue.textContent = this.formatNumber(convertedDailyTotal, decimals) + ` ${unitLabel}/day`;
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 => {
@@ -1227,8 +1604,24 @@
</div>
`;
} else {
const totalGramsForDays = breakdown.dailyGrams * numDays;
const convertedAmount = this.convertUnits(totalGramsForDays, unit);
// 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 = `<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}`;
}
return `
<div class="dog-calculator-food-amount-item">
@@ -1238,7 +1631,7 @@
${lockIndicator}
</div>
<div class="dog-calculator-food-amount-value">
${this.formatNumber(convertedAmount, decimals)} ${unitLabel}
${amountDisplay}
</div>
</div>
`;
@@ -1247,7 +1640,6 @@
// Calculate and display total
const totalFoodGrams = totalDailyGrams * numDays;
const totalConverted = this.convertUnits(totalFoodGrams, unit);
// Update the display
if (foodAmountsList) {
@@ -1255,12 +1647,31 @@
}
if (totalAmountDisplay) {
totalAmountDisplay.textContent = `${this.formatNumber(totalConverted, decimals)} ${unitLabel}`;
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();
foodAmountsSection.style.display = 'block';
this.sendHeightToParent();
}
getFoodSourceEnergyPer100g(foodSource) {
@@ -1286,13 +1697,18 @@
}
}
// Iframe auto-resize for allowed embeddings
setupIframeResize() {
// Send height to parent window for iframe auto-resize
this.sendHeightToParent();
// Only when embedded in an iframe
if (window.top === window.self) return;
// Monitor for content changes that might affect height
// Initial send once UI is ready
setTimeout(() => this.sendHeightToParent(), 50);
// Monitor for content/attribute changes
const observer = new MutationObserver(() => {
setTimeout(() => this.sendHeightToParent(), 100);
clearTimeout(this._resizeTimer);
this._resizeTimer = setTimeout(() => this.sendHeightToParent(), 100);
});
observer.observe(document.body, {
@@ -1301,18 +1717,25 @@
attributes: true
});
// Send height on window resize
// On viewport resize
window.addEventListener('resize', () => this.sendHeightToParent());
}
sendHeightToParent() {
const height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: 'dogCalculatorResize',
height: height
}, '*');
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
@@ -1321,50 +1744,25 @@
const shareUrl = document.getElementById('shareUrl');
if (modal && shareUrl) {
shareUrl.value = window.location.href;
modal.style.display = 'block';
// 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();
}
showEmbedModal() {
const modal = document.getElementById('embedModal');
const widgetCode = document.getElementById('widgetCode');
const iframeCode = document.getElementById('iframeCode');
if (modal && widgetCode && iframeCode) {
// Build embed URL
const baseUrl = window.location.protocol + '//embed.' + window.location.hostname;
// Create widget code using createElement to avoid quote issues
const scriptTag = document.createElement('script');
scriptTag.src = baseUrl + '/dog-calorie-calculator/dog-food-calculator-widget.js';
const divTag = document.createElement('div');
divTag.id = 'dog-calorie-calculator';
const widgetHtml = scriptTag.outerHTML + '\n' + divTag.outerHTML;
widgetCode.textContent = widgetHtml;
// Create iframe code using createElement
const iframe = document.createElement('iframe');
iframe.src = baseUrl + '/dog-calorie-calculator/iframe.html';
iframe.width = '100%';
iframe.height = '600';
iframe.frameBorder = '0';
iframe.title = 'Dog Calorie Calculator';
iframeCode.textContent = iframe.outerHTML;
modal.style.display = 'block';
}
}
// Embed modal removed (embedding disabled)
hideEmbedModal() {
const modal = document.getElementById('embedModal');
if (modal) modal.style.display = 'none';
}
// Embed modal removed (embedding disabled)
shareToFacebook() {
const url = encodeURIComponent(window.location.href);
@@ -1411,30 +1809,40 @@
}
}
async copyEmbedCode(type) {
const codeElement = document.getElementById(type === 'widget' ? 'widgetCode' : 'iframeCode');
const copyBtn = document.getElementById(type === 'widget' ? 'copyWidget' : 'copyIframe');
if (codeElement && copyBtn) {
try {
await navigator.clipboard.writeText(codeElement.textContent);
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
console.log('Copy fallback needed');
}
}
}
// 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 = '<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;
}
}
new DogCalorieCalculator();
});
File diff suppressed because it is too large Load Diff
-37
View File
@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Widget Test</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<h1>Dog Calculator Widget Test</h1>
<div class="test-container">
<h2>Test 1: Basic Widget</h2>
<div class="dog-calorie-calculator" data-theme="light" data-scale="0.9"></div>
</div>
<div class="test-container">
<h2>Test 2: Dark Theme Widget</h2>
<div class="dog-calorie-calculator" data-theme="dark" data-scale="0.5"></div>
</div>
<script src="sundog-dog-food-calculator.js"></script>
</body>
</html>