My theme for forester (+plugins)
at main 110 lines 3.6 kB view raw
1class GregoChant extends HTMLElement { 2 constructor() { 3 super(); 4 this._initialized = false; 5 6 // Attach shadow DOM 7 this.attachShadow({ mode: 'open' }); 8 this.shadowRoot.innerHTML = ` 9 <slot id="light-slot"></slot> 10 <div id="render"></div> 11 `; 12 13 this._slot = this.shadowRoot.querySelector('#light-slot'); 14 this._renderContainer = this.shadowRoot.querySelector('#render'); 15 16 // Always define a bound _doLayout, safe to call anytime 17 this._doLayout = () => { 18 if (!this.score) return; // layout only if score exists 19 this.score.performLayoutAsync(this._ctxt, () => { 20 this.score.layoutChantLines(this._ctxt, this.clientWidth, () => { 21 this._renderContainer.innerHTML = this.score.createSvg(this._ctxt); 22 // hide the slot content 23 this._slot.style.display = 'none'; 24 }); 25 }); 26 }; 27 28 this._onSlotChange = this._onSlotChange.bind(this); 29 this._onMutations = this._onMutations.bind(this); 30 } 31 32 connectedCallback() { 33 // Listen for slot changes 34 this._slot.addEventListener('slotchange', this._onSlotChange); 35 36 // MutationObserver for fallback dynamic content 37 this._mo = new MutationObserver(this._onMutations); 38 this._mo.observe(this, { childList: true, subtree: true, characterData: true }); 39 40 // ResizeObserver can safely call _doLayout now 41 this._resizeObserver = new ResizeObserver(this._doLayout); 42 this._resizeObserver.observe(this); 43 44 // Try initializing immediately in case content is already present 45 this._attemptInit(); 46 } 47 48 disconnectedCallback() { 49 this._slot.removeEventListener('slotchange', this._onSlotChange); 50 if (this._mo) this._mo.disconnect(); 51 if (this._resizeObserver) this._resizeObserver.disconnect(); 52 } 53 54 _gabcTextFromNodes(nodes) { 55 let text = ''; 56 for (const n of nodes) { 57 if (n.nodeType === Node.TEXT_NODE) text += n.textContent; 58 else if (n.nodeType === Node.ELEMENT_NODE) text += n.innerText || n.textContent || ''; 59 } 60 return text.trim(); 61 } 62 63 _onSlotChange() { this._attemptInit(); } 64 _onMutations() { this._attemptInit(); } 65 66 _attemptInit() { 67 if (this._initialized) return; 68 69 // prefer slotted nodes 70 const assigned = this._slot.assignedNodes({ flatten: true }) || []; 71 let gabc = assigned.length ? this._gabcTextFromNodes(assigned) : this.textContent.trim(); 72 if (!gabc) return; // still empty, wait for future content 73 74 this._initializeWithGabc(gabc); 75 } 76 77 _initializeWithGabc(gabc) { 78 if (this._initialized) return; 79 this._initialized = true; 80 81 this.score_src = gabc.replaceAll("GABCSPACE"," "); 82 console.log(this.score_src); 83 const ctxt = new exsurge.ChantContext(); 84 ctxt.lyricTextFont = "'Crimson Text', serif"; 85 ctxt.lyricTextSize *= 1.2; 86 ctxt.dropCapTextFont = ctxt.lyricTextFont; 87 ctxt.annotationTextFont = ctxt.lyricTextFont; 88 this._ctxt = ctxt; // store context for _doLayout 89 90 const mappings = exsurge.Gabc.createMappingsFromSource(ctxt, this.score_src); 91 const useDropCap = this.getAttribute('use-drop-cap') !== 'false'; 92 this.score = new exsurge.ChantScore(ctxt, mappings, useDropCap); 93 94 const annotationAttr = this.getAttribute('annotation'); 95 if (annotationAttr) { 96 this.score.annotation = new exsurge.Annotation(ctxt, annotationAttr); 97 } 98 99 // initial layout 100 this._doLayout(); 101 102 // After initialization, we can disconnect MutationObserver if desired 103 if (this._mo) { 104 this._mo.disconnect(); 105 this._mo = null; 106 } 107 } 108} 109 110window.customElements.define('grego-chant', GregoChant);