My theme for forester (+plugins)
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);