Home Manual Reference Source Test

src/view/controls.js

  1. /**
  2. * Bundle of form-control helpers wired into the Alpine "app" data object via spread.
  3. * @type {Object}
  4. * @property {function(params: Object, desc: {name: string, min: number, max: number}, delta: number): void} stepBy
  5. * Stepper +/- click handler: bump params[desc.name] by delta clamped to [desc.min, desc.max].
  6. * @property {function(parts: Object[], index: number, counter: string): boolean} isDuplicatePart
  7. * True when a parts-section row at index reuses an earlier label.
  8. * @property {function(parts: Object[], index: number): number} duplicateSourceIndex
  9. * 1-based index of the first earlier rho with the same label, or 0 when none.
  10. * @property {function(voice: {min_pitch: number, max_pitch: number}, fieldName: string): void} clampVoicePitchRange
  11. * Voice-config input handler: snap the just-edited pitch field to the other when the range inverts.
  12. * @property {function(el: HTMLInputElement, getValue: Function, setValue: Function): void} setupChordTonesTristate
  13. * Single checkbox cycling free/scale/chord (getValue: () => 'free'|'scale'|'chord'; setValue: (v) => void);
  14. * chord = checked, scale = indeterminate, free = neither.
  15. * @property {function(formEl: HTMLElement, triData: Object<string,Object<string,string>>): void} setupFeedbackTristates
  16. * Form-scan over fieldset[data-tri-group] input[data-tri]: cycle neutral/negative/positive;
  17. * mutates triData in place.
  18. */
  19. export const CONTROLS = {
  20. stepBy,
  21. isDuplicatePart,
  22. duplicateSourceIndex,
  23. clampVoicePitchRange,
  24. setupChordTonesTristate,
  25. setupFeedbackTristates,
  26. };
  27. /** @private */
  28. function stepBy(params, descriptor, delta) {
  29. const { name, min, max } = descriptor;
  30. const v = params[name] + delta;
  31. params[name] = Math.max(min, Math.min(max, v));
  32. }
  33. /** @private */
  34. function isDuplicatePart(parts, index, counter) {
  35. const inSection = counter === "parts" || counter === "step1.parts";
  36. return inSection && duplicateSourceIndex(parts, index) > 0;
  37. }
  38. /** @private */
  39. function duplicateSourceIndex(parts, index) {
  40. const list = parts ?? [];
  41. const label = list[index]?.label;
  42. return list.slice(0, index).findIndex((p) => label && p?.label === label) + 1;
  43. }
  44. /** @private */
  45. function clampVoicePitchRange(voice, fieldName) {
  46. if (voice.max_pitch < voice.min_pitch) {
  47. const other = fieldName === "min_pitch" ? "max_pitch" : "min_pitch";
  48. voice[fieldName] = voice[other];
  49. }
  50. }
  51. /** @private */
  52. function setupChordTonesTristate(el, getValue, setValue) {
  53. const states = ["free", "scale", "chord"];
  54. cycleOnChange(el, states, getValue, (v) => {
  55. setValue(v);
  56. el.checked = v === "chord";
  57. el.indeterminate = v === "scale";
  58. });
  59. }
  60. /** @private */
  61. function setupFeedbackTristates(formEl, triData) {
  62. const states = ["neutral", "negative", "positive"];
  63. for (const fieldset of formEl.querySelectorAll("fieldset[data-tri-group]")) {
  64. const group = fieldset.dataset.triGroup;
  65. triData[group] = {};
  66. const inputs = fieldset.querySelectorAll("input[type=checkbox][data-tri]");
  67. for (const input of inputs) {
  68. const name = input.name;
  69. triData[group][name] = states[0];
  70. cycleOnChange(
  71. input,
  72. states,
  73. () => triData[group][name],
  74. (v) => {
  75. triData[group][name] = v;
  76. applyFeedbackVisual(input, v);
  77. },
  78. );
  79. }
  80. }
  81. }
  82. /** @private Apply current value, then advance to next state on each change event. */
  83. function cycleOnChange(target, states, getValue, applyValue) {
  84. applyValue(getValue());
  85. target.addEventListener("change", () => {
  86. applyValue(states[(states.indexOf(getValue()) + 1) % states.length]);
  87. });
  88. }
  89. /** @private Visual side-effect for one feedback tristate: aria-invalid + class flip. */
  90. function applyFeedbackVisual(el, v) {
  91. el.checked = v === "positive";
  92. el.indeterminate = v === "negative";
  93. el.classList.remove("tristate-positive", "tristate-negative");
  94. if (v === "neutral") {
  95. el.removeAttribute("aria-invalid");
  96. return;
  97. }
  98. el.setAttribute("aria-invalid", v === "negative" ? "true" : "false");
  99. el.classList.add(`tristate-${v}`);
  100. }