/** * Client-side measurement toggle + recipe scaling. * Default: metric units, collapsed times, 1x scale. * * Each measurement span has: * data-default / data-alt — pre-computed metric/imperial text (1x) * data-scalable — JSON with base values in g/ml for scaling * data-measurement-type — weight|volume|temperature|dimension|time */ (function () { 'use strict'; // ─── State ────────────────────────────────────────────── var showingAlt = {}; var scale = 1; var measureByVolume = false; var TYPE_LABELS = { temperature: { toAlt: '°F', toDefault: '°C' }, weight: { toAlt: 'lb', toDefault: 'kg' }, volume: { toAlt: 'cups', toDefault: 'ml' }, dimension: { toAlt: 'inch', toDefault: 'cm' }, time: { toAlt: 'hr+min', toDefault: 'min' }, }; // ─── Formatting (mirrors plugin logic) ────────────────── var NO_SPACE = { 'g':1, 'kg':1, 'ml':1, '°C':1, '°F':1, 'cm':1, 'mm':1 }; function formatNumber(n) { if (Number.isInteger(n) || Math.abs(n - Math.round(n)) < 0.001) { return Math.round(n).toString(); } var decimals = Math.abs(n) >= 10 ? 1 : 2; return n.toFixed(decimals).replace(/\.?0+$/, ''); } function unitLabel(unit, plural) { var labels = { 'g':'g', 'kg':'kg', 'oz':'oz', 'lb':'lb', 'cup': plural ? 'cups' : 'cup', 'tablespoon':'tbsp', 'teaspoon':'tsp', 'ml':'ml', 'L':'L', 'quart': plural ? 'quarts' : 'quart', 'gallon': plural ? 'gallons' : 'gallon', 'pint': plural ? 'pints' : 'pint', 'fl oz':'fl oz', }; return labels[unit] || unit; } function formatVU(value, unit) { var label = unitLabel(unit, value !== 1); var space = NO_SPACE[unit] ? '' : ' '; return formatNumber(value) + space + label; } // ─── Smart Unit Selection ─────────────────────────────── function smartMetricWeight(g) { return g >= 1000 ? { v: g / 1000, u: 'kg' } : { v: g, u: 'g' }; } function smartImperialWeight(oz) { return oz >= 16 ? { v: oz / 16, u: 'lb' } : { v: oz, u: 'oz' }; } function smartMetricVolume(ml) { return ml >= 1000 ? { v: ml / 1000, u: 'L' } : { v: ml, u: 'ml' }; } function smartImperialVolume(ml) { if (ml >= 236.588) return { v: ml / 236.588, u: 'cup' }; if (ml >= 14.787) return { v: ml / 14.787, u: 'tablespoon' }; return { v: ml / 4.929, u: 'teaspoon' }; } // ─── Scale Computation ────────────────────────────────── function computeScaledText(data, scaleFactor, imperial) { var baseVal = data.base; var isRange = Array.isArray(baseVal); var prefix = data.approx ? '~' : ''; if (data.type === 'weight') { if (isRange) { var lo = baseVal[0] * scaleFactor; var hi = baseVal[1] * scaleFactor; var sLo, sHi; if (imperial) { sLo = smartImperialWeight(lo / 28.3495); sHi = smartImperialWeight(hi / 28.3495); } else { sLo = smartMetricWeight(lo); sHi = smartMetricWeight(hi); } if (sLo.u === sHi.u) { var space = NO_SPACE[sLo.u] ? '' : ' '; return prefix + formatNumber(sLo.v) + '-' + formatNumber(sHi.v) + space + unitLabel(sLo.u, sHi.v !== 1); } return prefix + formatVU(sLo.v, sLo.u) + '-' + formatVU(sHi.v, sHi.u); } var g = baseVal * scaleFactor; var s; if (imperial) { s = smartImperialWeight(g / 28.3495); } else { s = smartMetricWeight(g); } return prefix + formatVU(s.v, s.u); } if (data.type === 'volume') { if (isRange) { var lo = baseVal[0] * scaleFactor; var hi = baseVal[1] * scaleFactor; var sLo, sHi; if (imperial) { sLo = smartImperialVolume(lo); sHi = smartImperialVolume(hi); } else { sLo = smartMetricVolume(lo); sHi = smartMetricVolume(hi); } if (sLo.u === sHi.u) { var space = NO_SPACE[sLo.u] ? '' : ' '; return prefix + formatNumber(sLo.v) + '-' + formatNumber(sHi.v) + space + unitLabel(sLo.u, sHi.v !== 1); } return prefix + formatVU(sLo.v, sLo.u) + '-' + formatVU(sHi.v, sHi.u); } var ml = baseVal * scaleFactor; var s; if (imperial) { s = smartImperialVolume(ml); } else { s = smartMetricVolume(ml); } return prefix + formatVU(s.v, s.u); } if (data.type === 'count') { if (isRange) { var lo = baseVal[0] * scaleFactor; var hi = baseVal[1] * scaleFactor; return prefix + formatNumber(lo) + '-' + formatNumber(hi); } return prefix + formatNumber(baseVal * scaleFactor); } return null; } // ─── Update All Measurements ──────────────────────────── function updateAll() { var spans = document.querySelectorAll('span.measurement'); spans.forEach(function (span) { var type = span.getAttribute('data-measurement-type'); var isHybrid = span.getAttribute('data-hybrid') === 'true'; var useVolumeMode = isHybrid && measureByVolume; var effectiveType = useVolumeMode ? 'volume' : type; var imperial = !!showingAlt[effectiveType]; var scalableAttr = useVolumeMode ? 'data-volume-scalable' : 'data-scalable'; var defaultAttr = useVolumeMode ? 'data-volume-default' : 'data-default'; var altAttr = useVolumeMode ? 'data-volume-alt' : 'data-alt'; var scalableRaw = span.getAttribute(scalableAttr); if (scalableRaw && scale !== 1) { // Use scaling computation try { var data = JSON.parse(scalableRaw); var text = computeScaledText(data, scale, imperial); if (text) { span.textContent = text; return; } } catch (e) {} } // No scaling or not scalable — use pre-computed text var text = imperial ? span.getAttribute(altAttr) : span.getAttribute(defaultAttr); if (text) span.textContent = text; }); } // ─── Unit Toggle ──────────────────────────────────────── function toggleType(type) { showingAlt[type] = !showingAlt[type]; updateAll(); var btn = document.querySelector('.measurement-toggle-btn[data-toggle-type="' + type + '"]'); if (btn) { var labels = TYPE_LABELS[type]; btn.textContent = showingAlt[type] ? labels.toAlt : labels.toDefault; } } // ─── Measure Mode Toggle ─────────────────────────────── function setMeasureMode(mode) { measureByVolume = (mode === 'volume'); var btns = document.querySelectorAll('.measure-mode-btn'); btns.forEach(function (btn) { if (btn.getAttribute('data-mode') === mode) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); updateAll(); } // ─── Scale Toggle ─────────────────────────────────────── function setScale(newScale) { scale = newScale; updateAll(); updateScaleButtons(); } function updateScaleButtons() { var btns = document.querySelectorAll('.scale-btn'); btns.forEach(function (btn) { var val = parseFloat(btn.getAttribute('data-scale')); if (val === scale) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); } // ─── Init ─────────────────────────────────────────────── function init() { var container = document.querySelector('.measurement-toggles'); if (!container) return; // Detect which types have toggleable content var typeHasToggle = {}; var hasScalable = false; var hasHybrid = false; var spans = document.querySelectorAll('span.measurement'); spans.forEach(function (span) { var type = span.getAttribute('data-measurement-type'); if (!type) return; var def = span.getAttribute('data-default'); var alt = span.getAttribute('data-alt'); if (def !== alt) typeHasToggle[type] = true; if (span.getAttribute('data-scalable')) hasScalable = true; if (span.getAttribute('data-hybrid') === 'true') hasHybrid = true; }); // Hybrid measurements in volume mode need the volume metric/imperial toggle if (hasHybrid) { typeHasToggle['volume'] = true; } var hasAny = false; // Measure mode toggle (Weight vs Volume) — only if hybrid measurements exist if (hasHybrid) { hasAny = true; var measureRow = document.createElement('div'); measureRow.className = 'toggle-row'; var measureLabel = document.createElement('span'); measureLabel.className = 'measure-label'; measureLabel.textContent = 'Measure:'; measureRow.appendChild(measureLabel); var modes = [ { mode: 'weight', label: 'Weight' }, { mode: 'volume', label: 'Volume' }, ]; modes.forEach(function (m) { var btn = document.createElement('button'); btn.className = 'measure-mode-btn' + (m.mode === 'weight' ? ' active' : ''); btn.setAttribute('data-mode', m.mode); btn.textContent = m.label; btn.addEventListener('click', function () { setMeasureMode(m.mode); }); measureRow.appendChild(btn); }); container.appendChild(measureRow); } // Unit toggle buttons var unitRow = document.createElement('div'); unitRow.className = 'toggle-row'; var unitLabel = document.createElement('span'); unitLabel.className = 'unit-label'; unitLabel.textContent = 'Unit:'; unitRow.appendChild(unitLabel); var typeOrder = ['temperature', 'weight', 'volume', 'dimension', 'time']; typeOrder.forEach(function (type) { if (!typeHasToggle[type]) return; hasAny = true; showingAlt[type] = false; var btn = document.createElement('button'); btn.className = 'measurement-toggle-btn'; btn.setAttribute('data-toggle-type', type); btn.textContent = TYPE_LABELS[type].toDefault; btn.addEventListener('click', function () { toggleType(type); }); unitRow.appendChild(btn); }); if (hasAny) { container.appendChild(unitRow); } // Scale buttons if (hasScalable) { hasAny = true; var scaleRow = document.createElement('div'); scaleRow.className = 'toggle-row'; var scaleLabel = document.createElement('span'); scaleLabel.className = 'scale-label'; scaleLabel.textContent = 'Scale:'; scaleRow.appendChild(scaleLabel); var scales = [0.5, 1, 2, 3]; scales.forEach(function (s) { var btn = document.createElement('button'); btn.className = 'scale-btn' + (s === 1 ? ' active' : ''); btn.setAttribute('data-scale', s); btn.textContent = s + 'x'; btn.addEventListener('click', function () { setScale(s); var input = document.querySelector('.scale-input'); if (input) input.value = ''; }); scaleRow.appendChild(btn); }); var input = document.createElement('input'); input.type = 'number'; input.className = 'scale-input'; input.placeholder = 'Custom'; input.min = '0.1'; input.step = '0.1'; input.addEventListener('input', function () { var val = parseFloat(input.value); if (val > 0 && !isNaN(val)) { setScale(val); } }); scaleRow.appendChild(input); container.appendChild(scaleRow); } if (hasAny) { container.style.display = 'flex'; } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();