class GregoChant extends HTMLElement { constructor() { super(); this._initialized = false; // Attach shadow DOM this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = `
`; this._slot = this.shadowRoot.querySelector('#light-slot'); this._renderContainer = this.shadowRoot.querySelector('#render'); // Always define a bound _doLayout, safe to call anytime this._doLayout = () => { if (!this.score) return; // layout only if score exists this.score.performLayoutAsync(this._ctxt, () => { this.score.layoutChantLines(this._ctxt, this.clientWidth, () => { this._renderContainer.innerHTML = this.score.createSvg(this._ctxt); // hide the slot content this._slot.style.display = 'none'; }); }); }; this._onSlotChange = this._onSlotChange.bind(this); this._onMutations = this._onMutations.bind(this); } connectedCallback() { // Listen for slot changes this._slot.addEventListener('slotchange', this._onSlotChange); // MutationObserver for fallback dynamic content this._mo = new MutationObserver(this._onMutations); this._mo.observe(this, { childList: true, subtree: true, characterData: true }); // ResizeObserver can safely call _doLayout now this._resizeObserver = new ResizeObserver(this._doLayout); this._resizeObserver.observe(this); // Try initializing immediately in case content is already present this._attemptInit(); } disconnectedCallback() { this._slot.removeEventListener('slotchange', this._onSlotChange); if (this._mo) this._mo.disconnect(); if (this._resizeObserver) this._resizeObserver.disconnect(); } _gabcTextFromNodes(nodes) { let text = ''; for (const n of nodes) { if (n.nodeType === Node.TEXT_NODE) text += n.textContent; else if (n.nodeType === Node.ELEMENT_NODE) text += n.innerText || n.textContent || ''; } return text.trim(); } _onSlotChange() { this._attemptInit(); } _onMutations() { this._attemptInit(); } _attemptInit() { if (this._initialized) return; // prefer slotted nodes const assigned = this._slot.assignedNodes({ flatten: true }) || []; let gabc = assigned.length ? this._gabcTextFromNodes(assigned) : this.textContent.trim(); if (!gabc) return; // still empty, wait for future content this._initializeWithGabc(gabc); } _initializeWithGabc(gabc) { if (this._initialized) return; this._initialized = true; this.score_src = gabc.replaceAll("GABCSPACE"," "); console.log(this.score_src); const ctxt = new exsurge.ChantContext(); ctxt.lyricTextFont = "'Crimson Text', serif"; ctxt.lyricTextSize *= 1.2; ctxt.dropCapTextFont = ctxt.lyricTextFont; ctxt.annotationTextFont = ctxt.lyricTextFont; this._ctxt = ctxt; // store context for _doLayout const mappings = exsurge.Gabc.createMappingsFromSource(ctxt, this.score_src); const useDropCap = this.getAttribute('use-drop-cap') !== 'false'; this.score = new exsurge.ChantScore(ctxt, mappings, useDropCap); const annotationAttr = this.getAttribute('annotation'); if (annotationAttr) { this.score.annotation = new exsurge.Annotation(ctxt, annotationAttr); } // initial layout this._doLayout(); // After initialization, we can disconnect MutationObserver if desired if (this._mo) { this._mo.disconnect(); this._mo = null; } } } window.customElements.define('grego-chant', GregoChant);