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