djledda.de main
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
8.5 KiB

  1. import { css, DJElement, h, html, qs } from "./util";
  2. //@ts-check
  3. const MPG_T3_SYN = 25;
  4. const MPG_T4_SYN = 100;
  5. class GlobalEventBus {
  6. /**
  7. * @private
  8. * @type {{ onChange(): void }[]}
  9. */
  10. changeSubscribers = [];
  11. /**
  12. * @param {{ onChange(): void }} listener
  13. */
  14. addListener(listener) {
  15. this.changeSubscribers.push(listener);
  16. }
  17. /**
  18. * @param {any} sender
  19. */
  20. sendChange(sender) {
  21. for (const sub of this.changeSubscribers) {
  22. if (sub === sender) continue;
  23. sub.onChange();
  24. }
  25. }
  26. }
  27. const eventBus = new GlobalEventBus();
  28. class GrainInput {
  29. /** @type GrainInput[] */
  30. static inputs = [];
  31. /** @type string */
  32. name;
  33. /** @type string */
  34. unit;
  35. /** @type number */
  36. mpg;
  37. /** @type HTMLInputElement */
  38. inputEl;
  39. /** @type GrainInput */
  40. reference;
  41. /**
  42. * @param {{
  43. * name: string,
  44. * mpg: number,
  45. * reference: GrainInput | 'self',
  46. * unit: string,
  47. * step?: number,
  48. * }} opts
  49. */
  50. constructor(opts) {
  51. this.name = opts.name;
  52. this.mpg = opts.mpg;
  53. this.unit = opts.unit;
  54. this.reference = opts.reference === "self" ? this : opts.reference;
  55. this.inputEl = h("input", { type: "number", min: 0, step: opts.step ?? 1 });
  56. eventBus.addListener(this);
  57. }
  58. /**
  59. * @param {HTMLElement} insertionPoint
  60. */
  61. attachInput(insertionPoint) {
  62. insertionPoint.appendChild(this.inputEl);
  63. this.inputEl.valueAsNumber = this.mpg;
  64. this.inputEl.addEventListener("input", (e) => {
  65. const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
  66. if (this !== this.reference) {
  67. this.reference.inputEl.valueAsNumber = newVal / this.mpg;
  68. }
  69. eventBus.sendChange(this);
  70. });
  71. }
  72. onChange() {
  73. if (!this.reference || this === this.reference) return;
  74. this.inputEl.valueAsNumber = this.mpg * this.reference.inputEl.valueAsNumber;
  75. }
  76. getCurrentValue() {
  77. return this.inputEl.valueAsNumber;
  78. }
  79. }
  80. class RatiosController {
  81. /** @type HTMLInputElement */
  82. t3Ratio = h("input", { type: "number", min: 0, step: 1 });
  83. /** @type HTMLInputElement */
  84. t4Ratio = h("input", { type: "number", min: 0, step: 1 });
  85. /** @type HTMLInputElement */
  86. t3Syn = h("input", { type: "number", min: 0 });
  87. /** @type HTMLInputElement */
  88. t4Syn = h("input", { type: "number", min: 0 });
  89. /** @type number */
  90. t3 = 1;
  91. /** @type number */
  92. t4 = 4;
  93. /** @type GrainInput */
  94. reference;
  95. /**
  96. * @param {GrainInput} referenceInput
  97. */
  98. constructor(referenceInput) {
  99. this.reference = referenceInput;
  100. }
  101. /**
  102. * @param {Record<string, HTMLElement>} inputs
  103. */
  104. attachInputs(inputs) {
  105. inputs.t3Ratio.replaceWith(this.t3Ratio);
  106. inputs.t4Ratio.replaceWith(this.t4Ratio);
  107. inputs.t3Syn.replaceWith(this.t3Syn);
  108. inputs.t4Syn.replaceWith(this.t4Syn);
  109. this.t3Ratio.addEventListener("input", (e) => {
  110. const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
  111. this.t3 = newVal;
  112. this.onChange();
  113. });
  114. this.t4Ratio.addEventListener("input", (e) => {
  115. const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
  116. this.t4 = newVal;
  117. this.onChange();
  118. });
  119. this.t3Syn.addEventListener("input", (e) => {
  120. const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
  121. this.reference.inputEl.valueAsNumber = newVal / MPG_T3_SYN + this.t4Syn.valueAsNumber / MPG_T4_SYN;
  122. this.t3 = newVal;
  123. this.t4 = this.t4Syn.valueAsNumber;
  124. this.updateRatio();
  125. this.updateSyn();
  126. eventBus.sendChange(this);
  127. });
  128. this.t4Syn.addEventListener("input", (e) => {
  129. const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
  130. this.reference.inputEl.valueAsNumber = newVal / MPG_T4_SYN + this.t3Syn.valueAsNumber / MPG_T3_SYN;
  131. this.t3 = this.t3Syn.valueAsNumber;
  132. this.t4 = newVal;
  133. this.updateRatio();
  134. this.updateSyn();
  135. eventBus.sendChange(this);
  136. });
  137. eventBus.addListener(this);
  138. this.onChange();
  139. }
  140. onChange() {
  141. this.updateRatio();
  142. this.updateSyn();
  143. }
  144. updateRatio() {
  145. this.t3Ratio.valueAsNumber = this.t3;
  146. this.t4Ratio.valueAsNumber = this.t4;
  147. }
  148. updateSyn() {
  149. const total = this.t3 + this.t4;
  150. const t3Proportion = this.t3 / total;
  151. const t4Proportion = this.t4 / total;
  152. const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN;
  153. const multiplierSyn = this.reference.getCurrentValue() / grainsSyn;
  154. this.t3Syn.valueAsNumber = t3Proportion * multiplierSyn;
  155. this.t4Syn.valueAsNumber = t4Proportion * multiplierSyn;
  156. }
  157. }
  158. class ThyroidConverter extends DJElement {
  159. static styles = css`
  160. table {
  161. border-collapse: separate;
  162. margin: auto;
  163. margin-top: 20px;
  164. margin-bottom: 20px;
  165. border-radius: 5px;
  166. border-spacing: 0;
  167. border: 1px solid var(--text-color);
  168. }
  169. input {
  170. width: 70px;
  171. }
  172. tr:last-of-type td {
  173. border-bottom: none;
  174. }
  175. td {
  176. border-bottom: 1px solid var(--text-color);
  177. border-right: 1px solid var(--text-color);
  178. padding: 10px;
  179. }
  180. td:last-of-type {
  181. border-right: none;
  182. }
  183. .breathe {
  184. padding-left: 4px;
  185. padding-right: 4px;
  186. }
  187. @media (min-width: 600px) {
  188. td.right div {
  189. display: inline-block;
  190. }
  191. }
  192. `;
  193. static template = html`
  194. <table>
  195. <tbody id="table-body">
  196. <tr id="compounded-start" class="ratios">
  197. <td>
  198. Desired Ratio (T3:T4)
  199. </td>
  200. <td class="right">
  201. <div><slot class="t3"></slot></div> :
  202. <div><slot class="t4"></slot></div>
  203. </td>
  204. </tr>
  205. <tr class="synthetic">
  206. <td>
  207. Synthetic T3/T4 Combo
  208. </td>
  209. <td class="right">
  210. <div><slot class="t3"></slot>mcg</div> :
  211. <div><slot class="t4"></slot>mcg</div>
  212. </td>
  213. </tr>
  214. </tbody>
  215. </table>`;
  216. constructor() {
  217. super();
  218. const mainGrainInput = new GrainInput({
  219. name: "Grains",
  220. mpg: 1,
  221. reference: "self",
  222. unit: "",
  223. step: 0.1,
  224. });
  225. const ratiosController = new RatiosController(mainGrainInput);
  226. const inputs = [
  227. mainGrainInput,
  228. new GrainInput({
  229. name: "Armour, Natural Dessicated Thyroid",
  230. mpg: 60,
  231. unit: "mg",
  232. reference: mainGrainInput,
  233. }),
  234. new GrainInput({
  235. name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
  236. mpg: MPG_T3_SYN,
  237. unit: "mcg",
  238. reference: mainGrainInput,
  239. }),
  240. new GrainInput({
  241. name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
  242. mpg: MPG_T4_SYN,
  243. unit: "mcg",
  244. reference: mainGrainInput,
  245. }),
  246. ];
  247. const tableBody = qs("#table-body", this.root);
  248. const compoundedStart = qs("#compounded-start", this.root);
  249. for (const field of inputs) {
  250. const newRow = h("tr", {}, [
  251. h("td", { textContent: field.name }),
  252. h("td", {}, [
  253. h("div", { className: "input", style: "display: inline-block;" }),
  254. h("span", { className: "breathe", textContent: field.unit }),
  255. ]),
  256. ]);
  257. field.attachInput(qs("div.input", newRow));
  258. tableBody.insertBefore(newRow, compoundedStart);
  259. }
  260. ratiosController.attachInputs({
  261. t3Ratio: qs(".ratios .t3", tableBody),
  262. t4Ratio: qs(".ratios .t4", tableBody),
  263. t3Syn: qs(".synthetic .t3", tableBody),
  264. t4Syn: qs(".synthetic .t4", tableBody),
  265. });
  266. }
  267. }
  268. customElements.define("thyroid-converter", ThyroidConverter);