313 lines
No EOL
9.9 KiB
JavaScript
313 lines
No EOL
9.9 KiB
JavaScript
/**
|
|
* 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 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 imperial = !!showingAlt[type];
|
|
var scalableRaw = span.getAttribute('data-scalable');
|
|
|
|
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('data-alt')
|
|
: span.getAttribute('data-default');
|
|
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;
|
|
}
|
|
}
|
|
|
|
// ─── 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 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;
|
|
});
|
|
|
|
var hasAny = false;
|
|
|
|
// 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();
|
|
}
|
|
})(); |