299 lines
8.5 KiB
TypeScript
299 lines
8.5 KiB
TypeScript
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<string, HTMLElement>} 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`
|
|
<table>
|
|
<tbody id="table-body">
|
|
<tr id="compounded-start" class="ratios">
|
|
<td>
|
|
Desired Ratio (T3:T4)
|
|
</td>
|
|
<td class="right">
|
|
<div><slot class="t3"></slot></div> :
|
|
<div><slot class="t4"></slot></div>
|
|
</td>
|
|
</tr>
|
|
<tr class="synthetic">
|
|
<td>
|
|
Synthetic T3/T4 Combo
|
|
</td>
|
|
<td class="right">
|
|
<div><slot class="t3"></slot>mcg</div> :
|
|
<div><slot class="t4"></slot>mcg</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>`;
|
|
|
|
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);
|