'use strict'; /** * Alpine.js navigation helpers for conversion page. * Handles navigation via hidden AJAX link. */ function navActions() { return { navigate(url) { this.$refs.ajaxLink.href = url; this.$nextTick(() => this.$refs.ajaxLink.click()); }, navigateWithValue(url) { const el = document.getElementById('value'); const val = el ? el.value.replace(/,/g, '.') : '1'; this.$refs.ajaxLink.href = url + '?v=' + val; this.$nextTick(() => this.$refs.ajaxLink.click()); }, navigateWithResult(url) { const el = document.getElementById('result'); const val = el ? el.value.replace(/,/g, '.') : ''; if (val) { this.$refs.ajaxLink.href = url + '?v=' + val; } else { this.$refs.ajaxLink.href = url; } this.$nextTick(() => this.$refs.ajaxLink.click()); } }; } /** * Factory function for Alpine.js converter components. * @param {string} engine - 'linear', 'intermediate' or 'runtime' * @param {object} config - Engine-specific configuration * @returns {object} Alpine component data */ function createConverter(engine, config) { return { inputValue: '1', result: '', rates: {}, ratesError: '', async init() { const params = new URLSearchParams( window.location.search); const valueParam = params.get('v'); if (valueParam) { this.inputValue = valueParam.replace(/,/g, '.'); } this.$watch('inputValue', val => { if (val && val.indexOf(',') !== -1) { this.inputValue = val.replace(/,/g, '.'); } }); if (typeof config.init === 'function') { await config.init.call(this); } this.calculate(); }, calculate() { try { const normalized = parseNumber(this.inputValue); if (!normalized || isNaN(normalized)) { this.result = ''; return; } const value = new Decimal(normalized); let rawResult; if (typeof config.convert === 'function') { rawResult = config.convert.call(this, value); } else if (engine === 'linear') { const fromFactor = new Decimal( config.fromFactor); const toFactor = new Decimal( config.toFactor); rawResult = value.times(fromFactor) .dividedBy(toFactor); } else if (engine === 'intermediate') { let v = value.toNumber(); v = config.toIntermediate(v); v = config.fromIntermediate(v); rawResult = new Decimal(v); } else { this.result = ''; return; } this.result = prettyNumber(rawResult); const normalizedInputValue = this.inputValue .replace(/,/g, '.'); if (normalizedInputValue && normalizedInputValue !== '0') { history.replaceState( null, '', '?v=' + normalizedInputValue); } } catch (e) { this.result = ''; } } }; } /** * Formats a number as plain digits with dot as decimal separator. * @param {Decimal|string|number} num * @param {number} minPrecision - Minimum decimal places * @param {number} maxPrecision - Maximum decimal places * @returns {string} Formatted number string */ function prettyNumber(num, minPrecision, maxPrecision) { minPrecision = minPrecision || 4; maxPrecision = maxPrecision || 10; const d = new Decimal(num); const absVal = d.abs(); // Larger numbers need fewer decimal places for readable output; // smaller numbers get more to stay meaningful. let precision = maxPrecision; if (absVal.gte(1000000)) { precision = 2; } else if (absVal.gte(1000)) { precision = 4; } else if (absVal.gte(1)) { precision = 6; } else if (absVal.gte(0.001)) { precision = 8; } precision = Math.max(minPrecision, Math.min(precision, maxPrecision)); let str = d.toFixed(precision); if (str.indexOf('.') !== -1) { str = str.replace(/0+$/, ''); str = str.replace(/\.$/, ''); } return str; } /** * Parses a German number string. * Accepts both comma and dot as decimal separator. * @param {string} input * @returns {string} Normalized string with dot as decimal */ function parseNumber(input) { if (!input) return '0'; const hasComma = input.indexOf(',') !== -1; const hasDot = input.indexOf('.') !== -1; if (hasComma && hasDot) { return input.replace(/\./g, '').replace(/,/g, '.') || '0'; } return input.replace(/,/g, '.') || '0'; }