import { css, DJElement, h, html, qs } from "./util"; //@ts-check const MPG_T3_SYN = 25; const MPG_T4_SYN = 100; class GlobalEventBus { /** * @private * @type {{ onChange(): void }[]} */ changeSubscribers = []; /** * @param {{ onChange(): void }} listener */ addListener(listener) { this.changeSubscribers.push(listener); } /** * @param {any} sender */ sendChange(sender) { for (const sub of this.changeSubscribers) { if (sub === sender) continue; sub.onChange(); } } } const eventBus = new GlobalEventBus(); class GrainInput { /** @type GrainInput[] */ static inputs = []; /** @type string */ name; /** @type string */ unit; /** @type number */ mpg; /** @type HTMLInputElement */ inputEl; /** @type GrainInput */ reference; /** * @param {{ * name: string, * mpg: number, * reference: GrainInput | 'self', * unit: string, * step?: number, * }} opts */ constructor(opts) { this.name = opts.name; this.mpg = opts.mpg; this.unit = opts.unit; this.reference = opts.reference === "self" ? this : opts.reference; this.inputEl = h("input", { type: "number", min: 0, step: opts.step ?? 1 }); eventBus.addListener(this); } /** * @param {HTMLElement} insertionPoint */ attachInput(insertionPoint) { insertionPoint.appendChild(this.inputEl); this.inputEl.valueAsNumber = this.mpg; this.inputEl.addEventListener("input", (e) => { const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0; if (this !== this.reference) { this.reference.inputEl.valueAsNumber = newVal / this.mpg; } eventBus.sendChange(this); }); } onChange() { if (!this.reference || this === this.reference) return; this.inputEl.valueAsNumber = this.mpg * this.reference.inputEl.valueAsNumber; } getCurrentValue() { return this.inputEl.valueAsNumber; } } class RatiosController { /** @type HTMLInputElement */ t3Ratio = h("input", { type: "number", min: 0, step: 1 }); /** @type HTMLInputElement */ t4Ratio = h("input", { type: "number", min: 0, step: 1 }); /** @type HTMLInputElement */ t3Syn = h("input", { type: "number", min: 0 }); /** @type HTMLInputElement */ t4Syn = h("input", { type: "number", min: 0 }); /** @type number */ t3 = 1; /** @type number */ t4 = 4; /** @type GrainInput */ reference; /** * @param {GrainInput} referenceInput */ constructor(referenceInput) { this.reference = referenceInput; } /** * @param {Record} inputs */ attachInputs(inputs) { inputs.t3Ratio.replaceWith(this.t3Ratio); inputs.t4Ratio.replaceWith(this.t4Ratio); inputs.t3Syn.replaceWith(this.t3Syn); inputs.t4Syn.replaceWith(this.t4Syn); this.t3Ratio.addEventListener("input", (e) => { const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0; this.t3 = newVal; this.onChange(); }); this.t4Ratio.addEventListener("input", (e) => { const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0; this.t4 = newVal; this.onChange(); }); this.t3Syn.addEventListener("input", (e) => { const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0; this.reference.inputEl.valueAsNumber = newVal / MPG_T3_SYN + this.t4Syn.valueAsNumber / MPG_T4_SYN; this.t3 = newVal; this.t4 = this.t4Syn.valueAsNumber; this.updateRatio(); this.updateSyn(); eventBus.sendChange(this); }); this.t4Syn.addEventListener("input", (e) => { const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0; this.reference.inputEl.valueAsNumber = newVal / MPG_T4_SYN + this.t3Syn.valueAsNumber / MPG_T3_SYN; this.t3 = this.t3Syn.valueAsNumber; this.t4 = newVal; this.updateRatio(); this.updateSyn(); eventBus.sendChange(this); }); eventBus.addListener(this); this.onChange(); } onChange() { this.updateRatio(); this.updateSyn(); } updateRatio() { this.t3Ratio.valueAsNumber = this.t3; this.t4Ratio.valueAsNumber = this.t4; } updateSyn() { const total = this.t3 + this.t4; const t3Proportion = this.t3 / total; const t4Proportion = this.t4 / total; const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN; const multiplierSyn = this.reference.getCurrentValue() / grainsSyn; this.t3Syn.valueAsNumber = t3Proportion * multiplierSyn; this.t4Syn.valueAsNumber = t4Proportion * multiplierSyn; } } class ThyroidConverter extends DJElement { static styles = css` table { border-collapse: separate; margin: auto; margin-top: 20px; margin-bottom: 20px; border-radius: 5px; border-spacing: 0; border: 1px solid var(--text-color); } input { width: 70px; } tr:last-of-type td { border-bottom: none; } td { border-bottom: 1px solid var(--text-color); border-right: 1px solid var(--text-color); padding: 10px; } td:last-of-type { border-right: none; } .breathe { padding-left: 4px; padding-right: 4px; } @media (min-width: 600px) { td.right div { display: inline-block; } } `; static template = html`
Desired Ratio (T3:T4)
:
Synthetic T3/T4 Combo
mcg
:
mcg
`; constructor() { super(); const mainGrainInput = new GrainInput({ name: "Grains", mpg: 1, reference: "self", unit: "", step: 0.1, }); const ratiosController = new RatiosController(mainGrainInput); const inputs = [ mainGrainInput, new GrainInput({ name: "Armour, Natural Dessicated Thyroid", mpg: 60, unit: "mg", reference: mainGrainInput, }), new GrainInput({ name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)', mpg: MPG_T3_SYN, unit: "mcg", reference: mainGrainInput, }), new GrainInput({ name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)', mpg: MPG_T4_SYN, unit: "mcg", reference: mainGrainInput, }), ]; const tableBody = qs("#table-body", this.root); const compoundedStart = qs("#compounded-start", this.root); for (const field of inputs) { const newRow = h("tr", {}, [ h("td", { textContent: field.name }), h("td", {}, [ h("div", { className: "input", style: "display: inline-block;" }), h("span", { className: "breathe", textContent: field.unit }), ]), ]); field.attachInput(qs("div.input", newRow)); tableBody.insertBefore(newRow, compoundedStart); } ratiosController.attachInputs({ t3Ratio: qs(".ratios .t3", tableBody), t4Ratio: qs(".ratios .t4", tableBody), t3Syn: qs(".synthetic .t3", tableBody), t4Syn: qs(".synthetic .t4", tableBody), }); } } customElements.define("thyroid-converter", ThyroidConverter);