Widget:ConvertCalc2: Difference between revisions

From WyrdeWiki
No edit summary
No edit summary
Line 1: Line 1:
<noinclude>__NOTOC__
<noinclude>__NOTOC__
This widget converts with one neutral slider N and two editable fields.
This widget converts between two units with a single slider (slider always controls the *top* value).


=Usage=
=Usage=
{{#widget:ConvertCalc2
{{#widget:ConvertCalc2
| id=cc1
  | TopLabel=years
  | TopLabel=years
  | TopFactor=1.3125
  | TopFactor=1.3125
Line 12: Line 11:
  | SliderMax=100
  | SliderMax=100
  | SliderStep=0.1
  | SliderStep=0.1
| Decimals=1
| NumStep=0.1
}}
}}


{| class="wikitable"
{| class="wikitable"
|-
! param !! meaning !! default
! param !! meaning !! default
|-
|-
| TopLabel || label for the top unit || years
| TopLabel || label for the top unit || years
|-
|-
| TopFactor || multiplier for top value (Top = N * TopFactor) || 1.3125
| TopFactor || factor for the top unit (relative to a base) || 1.3125
|-
|-
| BottomLabel || label for the bottom unit || Cycles
| BottomLabel || label for the bottom unit || Cycles
|-
|-
| BottomFactor || multiplier for bottom value (Bottom = N * BottomFactor) || 1
| BottomFactor || factor for the bottom unit (relative to the same base) || 1
|-
|-
| SliderMin / SliderMax / SliderStep || slider range & step for N || 1 / 100 / 0.1
| SliderMin || slider minimum (top value) || 1
|-
|-
| Decimals || fixed decimals to display || 1
| SliderMax || slider maximum (top value) || 100
|-
|-
| NumStep || step on the number inputs || 0.1
| SliderStep || slider step || 0.1
|}
|}
</noinclude><includeonly>
</noinclude><includeonly>
<div class="wh-convert" style="max-width:32rem;margin:1rem 0;">
<div class="wh-cc2" style="max-width:720px;margin:1rem 0;">
   <!-- Top label -->
   <!-- Top label (capitalized) -->
   <div style="font-weight:700;margin-bottom:.25rem;" id="<!--{$id|default:'cc'|escape:'html'}-->-topLbl">
   <div id="cc2-topLabel" style="font-weight:700;text-transform:capitalize;"></div>
     <!--{$TopLabel|default:'years'|escape:'html'}-->
 
  <!-- Centered top value -->
  <div style="text-align:center;margin:.25rem 0 .5rem 0;">
     <span id="cc2-topVal" style="font-size:1.25rem;font-weight:600;">0</span>
    <span id="cc2-topUnit"></span>
   </div>
   </div>


   <!-- Top value (editable) -->
   <!-- Slider (controls top value) -->
   <div style="text-align:center;margin-bottom:.4rem;">
   <input id="cc2-slider" type="range" min="<!--{$SliderMin|default:1|escape:'html'}-->"
    <input id="<!--{$id|default:'cc'|escape:'html'}-->-topVal" type="number"
        max="<!--{$SliderMax|default:100|escape:'html'}-->"
          step="<!--{$NumStep|default:0.1|escape:'html'}-->" inputmode="decimal"
        step="<!--{$SliderStep|default:0.1|escape:'html'}-->"
          style="font:600 1.4rem/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
        value="<!--{$SliderMin|default:1|escape:'html'}-->"
                  width:12ch;padding:.15rem .35rem;text-align:right;">
        style="width:100%;">
    <span id="<!--{$id|default:'cc'|escape:'html'}-->-topUnit"
          style="font-weight:600;margin-left:.25rem;">
      <!--{$TopLabel|default:'years'|escape:'html'}-->
    </span>
  </div>


   <!-- Slider row -->
   <!-- Inputs row -->
   <div style="display:flex;align-items:center;gap:.5rem;margin:.25rem 0;">
   <div style="display:flex;gap:.75rem;align-items:center;margin:.5rem 0;">
     <input id="<!--{$id|default:'cc'|escape:'html'}-->-N" type="range"
     <label style="display:flex;gap:.35rem;align-items:center;flex:1;">
          min="<!--{$SliderMin|default:1|escape:'html'}-->"
      <span id="cc2-topLabelInline"></span>
          max="<!--{$SliderMax|default:100|escape:'html'}-->"
      <input id="cc2-topInput" type="number" inputmode="decimal" style="width:100%;padding:.25rem;">
          step="<!--{$SliderStep|default:0.1|escape:'html'}-->"
    </label>
          value="<!--{$SliderMin|default:1|escape:'html'}-->"
    <button id="cc2-swap" type="button" style="padding:.35rem .6rem;cursor:pointer;">⇅ swap</button>
          style="flex:1;">
    <label style="display:flex;gap:.35rem;align-items:center;flex:1;">
    <button id="<!--{$id|default:'cc'|escape:'html'}-->-swap"
      <span id="cc2-bottomLabelInline"></span>
            title="Swap units"
      <input id="cc2-bottomInput" type="number" inputmode="decimal" style="width:100%;padding:.25rem;">
            style="font-size:.95rem;padding:.2rem .5rem;cursor:pointer;">⇅</button>
    </label>
   </div>
   </div>


   <!-- Bottom value (editable) -->
   <!-- Centered bottom value -->
   <div style="text-align:center;margin:.4rem 0;">
   <div style="text-align:center;margin:.25rem 0 .5rem 0;">
     <input id="<!--{$id|default:'cc'|escape:'html'}-->-botVal" type="number"
     <span id="cc2-bottomVal" style="font-size:1.25rem;font-weight:600;">0</span>
          step="<!--{$NumStep|default:0.1|escape:'html'}-->" inputmode="decimal"
     <span id="cc2-bottomUnit"></span>
          style="font:600 1.4rem/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
                  width:12ch;padding:.15rem .35rem;text-align:right;">  
     <span id="<!--{$id|default:'cc'|escape:'html'}-->-botUnit"
          style="font-weight:600;margin-left:.25rem;">
      <!--{$BottomLabel|default:'Cycles'|escape:'html'}-->
    </span>
   </div>
   </div>


   <!-- Bottom label -->
   <!-- Bottom label (capitalized) -->
   <div style="font-weight:700;margin-top:.25rem;" id="<!--{$id|default:'cc'|escape:'html'}-->-botLbl">
   <div id="cc2-bottomLabel" style="font-weight:700;text-transform:capitalize;"></div>
    <!--{$BottomLabel|default:'Cycles'|escape:'html'}-->
  </div>
</div>
</div>


<script type="text/javascript">
<script type="text/javascript">
(function(){
(function(){
   var ID = "<!--{$id|default:'cc'|escape:'js'}-->";
  // pull params
   function ge(s){ return document.getElementById(ID + s); }
  var TOP_LABEL  = "<!--{$TopLabel|default:'years'|escape:'js'}-->";
  var TOP_FACTOR  = parseFloat("<!--{$TopFactor|default:1.3125|escape:'js'}-->") || 1.3125;
  var BOT_LABEL  = "<!--{$BottomLabel|default:'Cycles'|escape:'js'}-->";
  var BOT_FACTOR  = parseFloat("<!--{$BottomFactor|default:1|escape:'js'}-->") || 1;
 
   var SL_MIN  = parseFloat("<!--{$SliderMin|default:1|escape:'js'}-->") || 1;
  var SL_MAX  = parseFloat("<!--{$SliderMax|default:100|escape:'js'}-->") || 100;
  var SL_STEP = parseFloat("<!--{$SliderStep|default:0.1|escape:'js'}-->") || 0.1;
 
  // generate a unique suffix so multiple widgets can coexist without an id param
  var SUF = Math.random().toString(36).slice(2,7);
  function reid(id){ return id.replace('cc2-', 'cc2-' + SUF + '-'); }
   function ge(id){ return document.getElementById(reid(id)); }


   // Params
   // re-id all elements
   var TopLabel    = "<!--{$TopLabel|default:'years'|escape:'js'}-->";
   document.querySelectorAll('.wh-cc2 [id^="cc2-"]').forEach(function(n){
  var BottomLabel  = "<!--{$BottomLabel|default:'Cycles'|escape:'js'}-->";
    n.id = reid('#'+n.id).slice(1);
  var TopFactor    = parseFloat("<!--{$TopFactor|default:1.3125|escape:'js'}-->") || 1.3125;
   });
  var BottomFactor = parseFloat("<!--{$BottomFactor|default:1|escape:'js'}-->") || 1;
  var MIN  = parseFloat("<!--{$SliderMin|default:1|escape:'js'}-->") || 1;
  var MAX  = parseFloat("<!--{$SliderMax|default:100|escape:'js'}-->") || 100;
  var STEP = parseFloat("<!--{$SliderStep|default:0.1|escape:'js'}-->") || 0.1;
   var DEC  = parseInt("<!--{$Decimals|default:1|escape:'js'}-->") || 1;


  // Elements
   var el = {
   var el = {
     topLbl: ge("-topLbl"),
     topLabel: ge('cc2-topLabel'),
     botLbl: ge("-botLbl"),
    topUnit: ge('cc2-topUnit'),
     topUnit: ge("-topUnit"),
    topVal: ge('cc2-topVal'),
     botUnit: ge("-botUnit"),
    slider: ge('cc2-slider'),
     topVal: ge("-topVal"),
    topInput: ge('cc2-topInput'),
     botVal: ge("-botVal"),
     topInline: ge('cc2-topLabelInline'),
     N: ge("-N"),
 
     swap: ge("-swap")
     swap: ge('cc2-swap'),
 
     bottomUnit: ge('cc2-bottomUnit'),
     bottomVal: ge('cc2-bottomVal'),
     bottomInput: ge('cc2-bottomInput'),
     bottomInline: ge('cc2-bottomLabelInline'),
 
     bottomLabel: ge('cc2-bottomLabel')
  };
 
  // current state (which side is "top")
  var state = {
    topLabel: TOP_LABEL,
    topFactor: TOP_FACTOR,
    botLabel: BOT_LABEL,
    botFactor: BOT_FACTOR
   };
   };


   // Format helpers
   function effRatio(){ // bottom per 1 top (slider drives top)
   function clamp(v,min,max){ return Math.max(min, Math.min(max, v)); }
    return state.botFactor / state.topFactor;
   function fix(v){ return Number(v).toFixed(DEC); }
  }
 
   function fmt(n){
    // nice formatting: up to 4 decimal places, strip trailing zeros
    var s = (Math.abs(n) < 1e-4) ? "0" : n.toFixed(4);
    s = s.replace(/\.?0+$/,'');
    return s;
  }
 
   function refreshLabels(){
    el.topLabel.textContent = state.topLabel;
    el.bottomLabel.textContent = state.botLabel;


  // Core updates (N drives both)
     el.topUnit.textContent = ' ' + state.topLabel;
  function setFromN(n){
     el.topInline.textContent = state.topLabel + ':';
     n = clamp(n, MIN, MAX);
 
     el.N.value = n;
     el.bottomUnit.textContent = ' ' + state.botLabel;
     el.topVal.value = fix(n * TopFactor);
     el.bottomInline.textContent = state.botLabel + ':';
     el.botVal.value = fix(n * BottomFactor);
   }
   }


   // When top changes, derive N from top
   function updateFromTop(val){
  function setFromTop(x){
    // clamp to slider range
     var n = x / TopFactor;
    var v = Math.min(SL_MAX, Math.max(SL_MIN, val));
     setFromN(n);
    var r = effRatio();
     var bottom = v * r;
 
    el.slider.value = v;
    el.topInput.value = v;
    el.topVal.textContent = fmt(v);
 
    el.bottomInput.value = bottom;
     el.bottomVal.textContent = fmt(bottom);
   }
   }
  // When bottom changes, derive N from bottom
 
   function setFromBot(x){
   function updateFromBottom(bottomVal){
     var n = x / BottomFactor;
     var r = effRatio();
     setFromN(n);
    var top = bottomVal / r;
     updateFromTop(top);
   }
   }


   // Bind events (with light debouncing via rAF)
   function init(){
  var raf = null;
    // set slider bounds
  function queue(fn){ if(raf) return; raf = requestAnimationFrame(function(){ raf=null; fn(); }); }
    el.slider.min = SL_MIN; el.slider.max = SL_MAX; el.slider.step = SL_STEP; el.slider.value = SL_MIN;
 
    refreshLabels();
    updateFromTop(SL_MIN);
 
    // events
    el.slider.addEventListener('input', function(){
      updateFromTop(parseFloat(el.slider.value || SL_MIN));
    });


  el.N.addEventListener("input", function(){ queue(function(){ setFromN(parseFloat(el.N.value)); }); });
    el.topInput.addEventListener('input', function(){
  el.topVal.addEventListener("input", function(){ queue(function(){ setFromTop(parseFloat(el.topVal.value)); }); });
      var v = parseFloat(el.topInput.value);
  el.botVal.addEventListener("input", function(){ queue(function(){ setFromBot(parseFloat(el.botVal.value)); }); });
      if (isNaN(v)) return;
      updateFromTop(v);
    });


  // Swap labels & factors but preserve the visible numbers by keeping N constant
    el.bottomInput.addEventListener('input', function(){
  el.swap.addEventListener("click", function(){
      var v = parseFloat(el.bottomInput.value);
    // swap labels
      if (isNaN(v)) return;
    var tl = TopLabel, bl = BottomLabel;
      updateFromBottom(v);
    TopLabel = bl; BottomLabel = tl;
     });
    el.topLbl.textContent = TopLabel;
    el.topUnit.textContent = TopLabel;
    el.botLbl.textContent = BottomLabel;
     el.botUnit.textContent = BottomLabel;


     // swap factors
     el.swap.addEventListener('click', function(){
    var tf = TopFactor; TopFactor = BottomFactor; BottomFactor = tf;
      // swap labels & factors; keep current numerical *top* value as top
      var tl = state.topLabel, tf = state.topFactor;
      state.topLabel = state.botLabel; state.topFactor = state.botFactor;
      state.botLabel = tl;             state.botFactor = tf;


    // recompute from current N
      // after swap, slider still represents top; recompute bottom with new ratio
    setFromN(parseFloat(el.N.value));
      refreshLabels();
  });
      updateFromTop(parseFloat(el.topInput.value || el.slider.value || SL_MIN));
    });
  }


   // Init starting state
   if (document.readyState === 'loading'){
   setFromN(MIN);
    document.addEventListener('DOMContentLoaded', init, {once:true});
   } else { init(); }
})();
})();
</script>
</script>
</includeonly>
</includeonly>

Revision as of 23:17, 11 November 2025

This widget converts between two units with a single slider (slider always controls the *top* value).

Usage

years
years
Cycles
Cycles

param meaning default
TopLabel label for the top unit years
TopFactor factor for the top unit (relative to a base) 1.3125
BottomLabel label for the bottom unit Cycles
BottomFactor factor for the bottom unit (relative to the same base) 1
SliderMin slider minimum (top value) 1
SliderMax slider maximum (top value) 100
SliderStep slider step 0.1