import { Note } from "./scale.mjs"; // open string notes, starting from the deepest string. const strings = { string1: 'E', string2: 'A', string3: 'D', string4: 'G', string5: 'B', string6: 'E' }; // convert ‘string1’ ‘fret2’ in ‘form’ to F# export function fretToNote(form, stringName, fretName) { const string = strings[stringName]; if (!string) { return null; } if (form.querySelector(`.${stringName}.muted`)) { return 'x'; } else if (!fretName?.startsWith('fret')) { return form.querySelector(`.${stringName} .open`).getAttribute("x-data-note"); } else { return form.querySelector(`.${stringName} [value="${fretName}"]`).parentNode.getAttribute("x-data-note"); } } function formChanged(form) { const formData = new FormData(form); form.querySelectorAll('tbody .selected').forEach(elt => { const string = Array.from(elt.parentNode.classList).filter(x => x.startsWith('string'))[0]; const val = formData.get(string); const note = Note.fromString(fretToNote(form, string, val)); if (note.isSharp) { elt.innerText = `${note} / ${note.toAlternateString()}`; } else { elt.innerText = note; } }); } function handleFormChanged(e) { formChanged(e.target.form); } let mousedownVal = undefined; function fretMousedown(e) { // at mousedown time, the form hasn't yet been changed. to support // deselecting elements, we need to know what the previous // selection was, so store it here. // // it'd be nice to be able to do this just in the click handler. mousedownVal = e.target.checked; } function fretClick(e) { const elt = e.target; // if this element was selected at mousedown time, deselect it // now. if (mousedownVal) { elt.checked = false; // doesn't get called automatically. formChanged(elt.form); } } function openClick(e) { const elt = e.target.parentNode; if (elt.classList.contains('muted')) { elt.classList.remove('muted'); elt.querySelectorAll('input[type="radio"]').forEach(input => { input.disabled = false; }) } else { elt.classList.add('muted'); elt.querySelectorAll('input[type="radio"]').forEach(input => { input.checked = false; input.disabled = true; }) } formChanged(elt.closest('form')); } export default class extends HTMLElement { static name = 'x-fretboard'; static register() { console.debug('Registering Element', this.name, this); customElements.define(this.name, this); } saveEvent = 'x-fretboard-save'; constructor() { super(); console.debug('Fretboard#constructor', this); } connectedCallback() { console.debug('Fretboard#connectedCallback', this); // TODO: don't trawl the dom, custom element means we know // what's inside. this.form = this.querySelector('form'); this.form.onchange = handleFormChanged; this.form.querySelectorAll('.open').forEach(elt => { elt.onclick = openClick; }); this.form.querySelectorAll('input[type="radio"]').forEach(elt => { elt.onmousedown = fretMousedown; elt.onclick = fretClick; }); this.form.querySelectorAll('.save').forEach(elt => { elt.onclick = (e) => { console.log('Fretboard#onSave'); e.preventDefault(); const event = new CustomEvent(this.saveEvent, { bubbles: true, cancelable: true, detail: this }); this.dispatchEvent(event); }; }) formChanged(this.form); } get notes() { console.debug('Fretboard#notes', this); const formData = new FormData(this.form); return Array.from(this.form.querySelectorAll('tbody .selected')).map(elt => { const string = Array.from(elt.parentNode.classList).filter(x => x.startsWith('string'))[0]; const val = formData.get(string); return fretToNote(this.form, string, val); }) } }