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