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);