1/* Webinar Speaker Markup Normalizer 2==================================================*/ 3// Some webcast pages author speakers inline in the Content field 4// (<p><strong>Guest Speakers</strong></p> followed by <p><img><strong>Name<br> 5// Title<br>Company</strong></p> + bio paragraphs) instead of the structured 6// #about-presenters section. This rebuilds that inline markup into the same 7// structure so one set of styles (and the bio clamp below) applies everywhere. 8// Registered before the clamp handler so the section exists when it runs. 9(function () { 10 "use strict"; 11 12 document.addEventListener("DOMContentLoaded", () => { 13 if (document.getElementById("about-presenters")) return; 14 15 const content = document.querySelector("#webcast .lcsContent"); 16 if (!content) return; 17 18 // The CMS nests block elements inside the Content <p>; the browser 19 // splits them into siblings, so everything we need is a child of 20 // .lcsContent. Find the "Guest Speakers" heading paragraph. 21 const heading = Array.from(content.querySelectorAll("p")).find( 22 (p) => 23 p.querySelector("strong") && 24 !p.querySelector("img") && 25 /^guest speakers?$/i.test(p.textContent.trim()) 26 ); 27 if (!heading) return; 28 29 // Collect everything from the heading to the on-demand marker (or the 30 // first non-content element) â speaker paragraphs start with an <img>. 31 const collected = []; 32 let node = heading.nextElementSibling; 33 while (node && !node.classList.contains("ondemand") && /^(P|UL)$/.test(node.tagName)) { 34 // Skip metadata paragraphs (id ends in StartDate/StartTime, prefix 35 // varies per placeholder) so they don't land in a speaker bio. 36 if (!/(StartDate|StartTime)$/.test(node.id)) collected.push(node); 37 node = node.nextElementSibling; 38 } 39 40 // Group into speakers: an <img> paragraph opens a speaker, following 41 // paragraphs are their bio. 42 const speakers = []; 43 collected.forEach((el) => { 44 if (el.tagName === "P" && el.querySelector("img")) { 45 speakers.push({ intro: el, bio: [] }); 46 } else if (speakers.length) { 47 speakers[speakers.length - 1].bio.push(el); 48 } 49 }); 50 if (!speakers.length) return; 51 52 const section = document.createElement("section"); 53 section.id = "about-presenters"; 54 55 const title = document.createElement("p"); 56 title.className = "presenters-title"; 57 title.textContent = heading.textContent.trim(); 58 section.appendChild(title); 59 60 speakers.forEach(({ intro, bio }) => { 61 const wrapper = document.createElement("div"); 62 wrapper.className = "presenter-info-wrapper"; 63 64 const info = document.createElement("div"); 65 info.className = "presenter-info"; 66 67 // Name / title / company come from the <strong> block, one per 68 // <br>-separated line. 69 const strong = intro.querySelector("strong"); 70 const lines = strong 71 ? strong.innerHTML 72 .split(/<br\s*\/?>/i) 73 .map((line) => { 74 const probe = document.createElement("div"); 75 probe.innerHTML = line; 76 return probe.textContent.replace(/\s+/g, " ").trim(); 77 }) 78 .filter(Boolean) 79 : []; 80 81 const img = intro.querySelector("img"); 82 const photo = document.createElement("img"); 83 photo.className = "presenter-photo"; 84 photo.src = img.getAttribute("src"); 85 photo.alt = img.getAttribute("alt") || lines[0] || ""; 86 info.appendChild(photo); 87 88 ["presenter-name", "presenter-title", "presenter-company"].forEach( 89 (cls, i) => { 90 if (!lines[i]) return; 91 const p = document.createElement("p"); 92 p.className = cls; 93 p.textContent = lines[i]; 94 info.appendChild(p); 95 } 96 ); 97 98 const bioWrap = document.createElement("div"); 99 bioWrap.className = "presenter-bio";
100 bio.forEach((el) => bioWrap.appendChild(el)); 101 102 wrapper.appendChild(info); 103 wrapper.appendChild(bioWrap); 104 section.appendChild(wrapper); 105 }); 106 107 // Match the structured template: the section sits after the 108 // on-demand marker when present, otherwise where the heading was. 109 const onDemand = content.querySelector("p.ondemand"); 110 if (onDemand) { 111 onDemand.after(section); 112 } else { 113 heading.before(section); 114 } 115 116 heading.remove(); 117 // Bio paragraphs were moved into the section â only remove what's 118 // still sitting in the original content flow (the intro paragraphs). 119 collected.forEach((el) => { 120 if (!section.contains(el)) el.remove(); 121 }); 122 }); 123})(); 124 125/* Webinar Speaker Bios (Inner Pages) 126==================================================*/ 127// Clamps long speaker bios (#about-presenters) and adds a quiet 128// "Show more" toggle â only when the bio actually overflows the clamp. 129(function () { 130 "use strict"; 131 132 document.addEventListener("DOMContentLoaded", () => { 133 const bios = document.querySelectorAll("#about-presenters .presenter-bio"); 134 135 bios.forEach((bio) => { 136 // Idempotent: skip bios already processed (page may load this 137 // script twice â e.g. template include + media-library include) 138 if (bio.nextElementSibling?.classList.contains("presenter-bio-toggle")) return; 139 140 bio.classList.add("is-clamped"); 141 142 // No overflow at the clamp height â no toggle needed 143 if (bio.scrollHeight <= bio.clientHeight) { 144 bio.classList.remove("is-clamped"); 145 return; 146 } 147 148 const toggle = document.createElement("button"); 149 toggle.type = "button"; // Sitecore pages submit on bare <button> 150 toggle.className = "presenter-bio-toggle"; 151 toggle.textContent = "Show more"; 152 toggle.setAttribute("aria-expanded", "false"); 153 154 toggle.addEventListener("click", () => { 155 const expanded = bio.classList.toggle("is-expanded"); 156 toggle.textContent = expanded ? "Show less" : "Show more"; 157 toggle.setAttribute("aria-expanded", String(expanded)); 158 }); 159 160 bio.after(toggle); 161 }); 162 }); 163})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.