Widget:ConvertCalc2: Difference between revisions

From WyrdeWiki
No edit summary
mNo edit summary
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
<noinclude>__NOTOC__
<noinclude>__NOTOC__
This widget converts between two units with a single slider (slider always controls the *top* value).
This widget converts between two units with a single slider.


=Usage=
=Usage=
Line 12: Line 12:
  | SliderStep=0.1
  | SliderStep=0.1
}}
}}
{| class="wikitable"
{| class="wikitable"
! param !! meaning !! default
! param !! meaning !! default
|-
|-
| TopLabel || label for the top unit || years
| TopLabel || name shown for the top value || years
|-
|-
| TopFactor || factor for the top unit (relative to a base) || 1.3125
| TopFactor || factor for 1 Top unit (used in bottom = top * Bottom/Top) || 1.3125
|-
|-
| BottomLabel || label for the bottom unit || Cycles
| BottomLabel || name shown for the bottom value || Cycles
|-
|-
| BottomFactor || factor for the bottom unit (relative to the same base) || 1
| BottomFactor || factor for 1 Bottom unit || 1
|-
|-
| SliderMin || slider minimum (top value) || 1
| SliderMin || minimum slider value (top value) || 1
|-
|-
| SliderMax || slider maximum (top value) || 100
| SliderMax || maximum slider value (top value) || 100
|-
|-
| SliderStep || slider step || 0.1
| SliderStep || slider step || 0.1
|}
|}
</noinclude><includeonly>
</noinclude><includeonly>
<div class="wh-cc2" style="max-width:720px;margin:1rem 0;">
<div class="wh-convertcalc2" style="max-width:720px;margin:1rem 0;">
   <!-- Top label (capitalized) -->
   <!-- Top title (Capitalized) -->
   <div id="cc2-topLabel" style="font-weight:700;text-transform:capitalize;"></div>
   <div id="cc2-title-top" style="font-weight:700;margin-bottom:.35rem;text-transform:capitalize;">years</div>


   <!-- Centered top value -->
   <!-- Top value centered above slider -->
   <div style="text-align:center;margin:.25rem 0 .5rem 0;">
   <div style="display:flex;justify-content:center;margin-bottom:.4rem;">
     <span id="cc2-topVal" style="font-size:1.25rem;font-weight:600;">0</span>
     <input id="cc2-topval" type="number" step="0.01" style="width:10ch;text-align:right;margin-right:.4rem;">
     <span id="cc2-topUnit"></span>
     <span id="cc2-toplabel">years</span>
   </div>
   </div>


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


   <!-- Centered bottom value -->
   <!-- Bottom value centered below slider -->
   <div style="text-align:center;margin:.25rem 0 .5rem 0;">
   <div style="display:flex;justify-content:center;margin:.5rem 0 .6rem;">
     <span id="cc2-bottomVal" style="font-size:1.25rem;font-weight:600;">0</span>
     <input id="cc2-botval" type="number" step="0.01" style="width:10ch;text-align:right;margin-right:.4rem;" readonly>
     <span id="cc2-bottomUnit"></span>
     <span id="cc2-botlabel">Cycles</span>
   </div>
   </div>


   <!-- Bottom label (capitalized) -->
   <!-- Bottom title (Capitalized) -->
   <div id="cc2-bottomLabel" style="font-weight:700;text-transform:capitalize;"></div>
   <div id="cc2-title-bot" style="font-weight:700;text-transform:capitalize;">Cycles</div>
</div>
</div>


<script type="text/javascript">
<script type="text/javascript">
(function(){
(function(){
   // pull params
   // Pull parameters (only the ones you defined)
   var TOP_LABEL  = "<!--{$TopLabel|default:'years'|escape:'js'}-->";
   var TopLabel    = "<!--{$TopLabel|default:'years'|escape:'js'}-->";
   var TOP_FACTOR  = parseFloat("<!--{$TopFactor|default:1.3125|escape:'js'}-->") || 1.3125;
   var TopFactor    = parseFloat("<!--{$TopFactor|default:1.3125|escape:'js'}-->") || 1.3125;
   var BOT_LABEL  = "<!--{$BottomLabel|default:'Cycles'|escape:'js'}-->";
   var BottomLabel  = "<!--{$BottomLabel|default:'Cycles'|escape:'js'}-->";
   var BOT_FACTOR  = parseFloat("<!--{$BottomFactor|default:1|escape:'js'}-->") || 1;
   var BottomFactor = 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)); }
 
  // re-id all elements
  document.querySelectorAll('.wh-cc2 [id^="cc2-"]').forEach(function(n){
    n.id = reid('#'+n.id).slice(1);
  });
 
  var el = {
    topLabel: ge('cc2-topLabel'),
    topUnit: ge('cc2-topUnit'),
    topVal: ge('cc2-topVal'),
    slider: ge('cc2-slider'),
    topInput: ge('cc2-topInput'),
    topInline: ge('cc2-topLabelInline'),
 
    swap: ge('cc2-swap'),
 
    bottomUnit: ge('cc2-bottomUnit'),
    bottomVal: ge('cc2-bottomVal'),
    bottomInput: ge('cc2-bottomInput'),
    bottomInline: ge('cc2-bottomLabelInline'),


    bottomLabel: ge('cc2-bottomLabel')
  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;


   // current state (which side is "top")
   // Simple helpers
   var state = {
  function ge(id){ return document.getElementById(id); }
    topLabel: TOP_LABEL,
   function cap(s){ return (s||'').slice(0,1).toUpperCase() + (s||'').slice(1); }
    topFactor: TOP_FACTOR,
  function fix2(n){ return (Math.round(n*100)/100).toFixed(2); }
    botLabel: BOT_LABEL,
  function clamp(x,a,b){ return Math.min(b, Math.max(a, x)); }
    botFactor: BOT_FACTOR
  };


   function effRatio(){ // bottom per 1 top (slider drives top)
   // Elements
     return state.botFactor / state.topFactor;
  var eTopTitle = ge("cc2-title-top");
  }
  var eBotTitle = ge("cc2-title-bot");
  var eTopLbl  = ge("cc2-toplabel");
  var eBotLbl  = ge("cc2-botlabel");
  var eTopVal  = ge("cc2-topval");
  var eBotVal  = ge("cc2-botval");
  var eSlider  = ge("cc2-slider");
  var eSwap     = ge("cc2-swap");


   function fmt(n){
  // Init labels/titles
     // nice formatting: up to 4 decimal places, strip trailing zeros
   function paintLabels(){
    var s = (Math.abs(n) < 1e-4) ? "0" : n.toFixed(4);
     eTopTitle.textContent = cap(TopLabel);
     s = s.replace(/\.?0+$/,'');
    eBotTitle.textContent = cap(BottomLabel);
     return s;
     eTopLbl.textContent  = TopLabel;
     eBotLbl.textContent  = BottomLabel;
   }
   }
  paintLabels();


   function refreshLabels(){
   // Slider config (slider always controls TOP value)
    el.topLabel.textContent = state.topLabel;
  eSlider.min  = SL_MIN;
    el.bottomLabel.textContent = state.botLabel;
  eSlider.max  = SL_MAX;
  eSlider.step = SL_STEP;
  eSlider.value = SL_MIN;


    el.topUnit.textContent = ' ' + state.topLabel;
  // Set initial numbers
    el.topInline.textContent = state.topLabel + ':';
  eTopVal.value = fix2(parseFloat(eSlider.value));
  eBotVal.value = fix2(parseFloat(eTopVal.value) * (BottomFactor / TopFactor));


     el.bottomUnit.textContent = ' ' + state.botLabel;
  // Update cascade from top value (slider or manual input)
     el.bottomInline.textContent = state.botLabel + ':';
  function fromTop(v){
     var topNum = clamp(parseFloat(v)||0, SL_MIN, SL_MAX);
    eSlider.value = topNum;
    eTopVal.value = fix2(topNum);
    var bottom = topNum * (BottomFactor / TopFactor);
     eBotVal.value = fix2(bottom);
   }
   }


   function updateFromTop(val){
   // Events
    // clamp to slider range
  eSlider.addEventListener("input", function(){ fromTop(this.value); });
    var v = Math.min(SL_MAX, Math.max(SL_MIN, val));
  eTopVal.addEventListener("input", function(){ fromTop(this.value); });
    var r = effRatio();
    var bottom = v * r;


    el.slider.value = v;
   // Swap button: swap labels & factors; slider still drives TOP
    el.topInput.value = v;
  eSwap.addEventListener("click", function(){
    el.topVal.textContent = fmt(v);
     var tl = TopLabel; TopLabel = BottomLabel; BottomLabel = tl;
 
     var tf = TopFactor; TopFactor = BottomFactor; BottomFactor = tf;
    el.bottomInput.value = bottom;
     paintLabels();
    el.bottomVal.textContent = fmt(bottom);
     // keep same numeric top value; just recompute bottom
  }
    fromTop(eTopVal.value);
 
   });
   function updateFromBottom(bottomVal){
    var r = effRatio();
    var top = bottomVal / r;
    updateFromTop(top);
  }
 
  function init(){
    // set slider bounds
    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.topInput.addEventListener('input', function(){
      var v = parseFloat(el.topInput.value);
      if (isNaN(v)) return;
      updateFromTop(v);
     });
 
    el.bottomInput.addEventListener('input', function(){
      var v = parseFloat(el.bottomInput.value);
      if (isNaN(v)) return;
      updateFromBottom(v);
     });
 
     el.swap.addEventListener('click', function(){
      // 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;
 
      // after swap, slider still represents top; recompute bottom with new ratio
      refreshLabels();
      updateFromTop(parseFloat(el.topInput.value || el.slider.value || SL_MIN));
    });
  }
 
  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', init, {once:true});
   } else { init(); }
})();
})();
</script>
</script>
</includeonly>
</includeonly>

Latest revision as of 23:35, 11 November 2025

This widget converts between two units with a single slider.

Usage

years
years
Cycles
Cycles

param meaning default
TopLabel name shown for the top value years
TopFactor factor for 1 Top unit (used in bottom = top * Bottom/Top) 1.3125
BottomLabel name shown for the bottom value Cycles
BottomFactor factor for 1 Bottom unit 1
SliderMin minimum slider value (top value) 1
SliderMax maximum slider value (top value) 100
SliderStep slider step 0.1