1/* ============================================================================= 2 * IBS 2027 Redesign - Section-Component JS 3 * 4 * Progressive-enhancement vanilla JS for section components on Home/Default 5 * templates. Loaded only by templates that render <vc:...> section components 6 * (via sections-bundle.js); not loaded on shells that have no sections 7 * (login, ExhibitorTools, simple legacy pages). 8 * 9 * Conventions: 10 * - One IIFE per section component (keep handlers narrowly scoped). 11 * - Guard with element-exists checks; sections only exist on pages where 12 * the editor added them. 13 * - owl.carousel JS is NOT in sections-bundle yet - it still ships via 14 * js/2023/home-bundle.js (Home) and js/2023/inner-bundle.js (Default). 15 * When the last legacy bundle reference is removed, add 16 * node_modules/owl.carousel/dist/owl.carousel.min.js to sections-bundle 17 * in bundleconfig.json. 18 * ============================================================================= */ 19 20(function () { 21 'use strict'; 22 23 /* 24 * sections-bundle.js loads in document order alongside legacy bundles that 25 * extend jQuery (e.g. owl.carousel via 2023/home-bundle.js). For deferred 26 * scripts, our IIFE runs while readyState is 'interactive' -- BEFORE the 27 * sibling defer scripts have executed. We must therefore wait for 28 * DOMContentLoaded unless the page is already fully loaded, otherwise 29 * dependent globals (jQuery plugins, loadSpecificVideo) may be missing. 30 */ 31 function onReady(callback) { 32 if (document.readyState === 'complete') { 33 callback(); 34 return; 35 } 36 37 document.addEventListener('DOMContentLoaded', callback); 38 } 39 40 onReady(function () { 41 initHpHero(); 42 initHpNews(); 43 initFeaturedSpeakers(); 44 initTestimonialsCarousel(); 45 initEducationSessions(); 46 }); 47 48 /* ========================================================================= 49 * HP Hero - homepage Gradient Hero / slider 50 * 51 * When multiple slides, .hp-hero__track gets .owl-carousel and owl drives 52 * it. The .hp-hero__pagination cluster is reparented into the active 53 * slide's .hp-hero__controls-slot on every translate so it sits inside 54 * the gradient panel alongside the CTA button. 55 * ========================================================================= */ 56 function initHpHero() { 57 var section = document.querySelector('[data-hp-hero]'); 58 if (!section || typeof window.jQuery === 'undefined') { 59 return; 60 } 61 62 var $ = window.jQuery; 63 var $section = $(section); 64 var $track = $section.find('.hp-hero__track').first(); 65 var $pagination = $section.find('.hp-hero__pagination').first(); 66 var hasMultiple = $track.hasClass('owl-carousel'); 67 68 if (hasMultiple) { 69 // Pre-init we keep slides 2+ hidden to avoid a flash of stacked content. 70 var $slides = $track.find('.hp-hero__slide').show(); 71 // .hp-hero__dots is static markup (only the .hp-hero__pagination 72 // wrapper gets reparented), so cache the dot collection once. 73 var $dots = $pagination.find('.hp-hero__dot'); 74 75 var syncControls = function () { 76 var $activeItem = $track.find('.owl-item.active').first(); 77 if (!$activeItem.length) { 78 return; 79 } 80 81 // Reparent pagination into the visible active item. Clones are 82 // structural copies of the real slide markup, so their 83 // controls-slot works the same. 84 $activeItem.find('.hp-hero__controls-slot').first().append($pagination); 85 86 // Real slide index for the dots. On a swipe-end past the last 87 // slide, owl's onDragEnd lands on a clone (closest()) and 88 // never silently snaps back to the real item the way to() 89 // does for click-driven nav - so we can't trust the DOM 90 // position of $activeItem. owl's relative() helper converts 91 // an absolute (clone-inclusive) position to the real index. 92 var carousel = $track.data('owl.carousel'); 93 var realIndex; 94 if (carousel && typeof carousel.current === 'function') { 95 realIndex = carousel.relative(carousel.current()); 96 } else { 97 // First `initialized` fire-through: owl data isn't on the 98 // element yet, so derive the index from the DOM. This 99 // branch is only hit on first paint, never on swipe. 100 realIndex = $track.find('.owl-item').not('.cloned').index($activeItem); 101 } 102 103 if (typeof realIndex !== 'number' || realIndex < 0) { 104 return; 105 } 106 107 $dots.removeClass('is-active').attr('aria-pressed', 'false');
108 $dots.filter('[data-slide-index="' + realIndex + '"]') 109 .addClass('is-active') 110 .attr('aria-pressed', 'true'); 111 }; 112 113 $track.on('initialized.owl.carousel translated.owl.carousel refreshed.owl.carousel', syncControls); 114 115 // Before owl rebuilds its loop clones, pull the live pagination back 116 // out to the section root. owl regenerates clones by serializing the 117 // real items' outerHTML (owl.carousel.js); if the pagination is 118 // sitting inside a real slide's controls-slot at clone time, a static 119 // copy gets baked into the clones and renders as duplicate controls. 120 // 'refresh' fires *before* the re-clone, so detaching here keeps the 121 // serialized real items clean; the 'refreshed' handler above 122 // re-inserts the live pagination into the active slide afterward. 123 // refresh() is driven by owl's onResize, which only fires on an 124 // actual track-width change (e.g. a scrollbar appearing on late 125 // content load) - which is why the duplicate is intermittent. 126 $track.on('refresh.owl.carousel', function () { 127 $section.append($pagination); 128 }); 129 130 $track.owlCarousel({ 131 items: 1, 132 loop: true, 133 nav: false, 134 dots: false, 135 autoHeight: false, 136 autoplay: true, 137 autoplayTimeout: 7000, 138 autoplayHoverPause: true 139 }); 140 141 // Owl's `initialized` event sometimes fires before our handler is 142 // bound on first paint; this manual call guarantees the pagination 143 // lands in the active slot before the user can see the hero. 144 syncControls(); 145 146 var equalizeSlideHeights = function () { 147 $slides.css('min-height', ''); 148 var max = 0; 149 $slides.each(function () { 150 var h = $(this).outerHeight(true); 151 if (h > max) { max = h; } 152 }); 153 if (max > 0) { 154 $slides.css('min-height', max + 'px'); 155 } 156 }; 157 158 equalizeSlideHeights(); 159 // Coalesce resize bursts with rAF so we don't reflow N times during 160 // a drag-resize. 161 var resizeFrame = 0; 162 $(window).on('resize.hpHero', function () { 163 if (resizeFrame) { 164 return; 165 } 166 resizeFrame = window.requestAnimationFrame(function () { 167 resizeFrame = 0; 168 equalizeSlideHeights(); 169 }); 170 }); 171 172 // Pagination is reparented inside the carousel on each translate, so 173 // delegate the click handlers to the section root. 174 $section.on('click', '.hp-hero__nav--prev', function () { 175 $track.trigger('prev.owl.carousel'); 176 }); 177 $section.on('click', '.hp-hero__nav--next', function () { 178 $track.trigger('next.owl.carousel'); 179 }); 180 $section.on('click', '.hp-hero__dot', function () { 181 var idx = parseInt(this.getAttribute('data-slide-index'), 10); 182 if (isNaN(idx)) { 183 return; 184 } 185 $track.trigger('to.owl.carousel', [idx, 300, true]); 186 }); 187 } 188 189 if (section.hasAttribute('data-hp-hero-video') && typeof window.loadSpecificVideo === 'function') { 190 // The play link carries data-video-id-tmp (not data-video-id) so the 191 // legacy generic $("a[data-video-id]") inline handler in home.js/inner.js 192 // ignores it and this delegated handler is the sole, clone-safe authority. 193 var isDesktop = function () { 194 return window.matchMedia('(min-width: 768px)').matches; 195 }; 196 197 var modalId = section.getAttribute('data-hp-hero-video-modal'); 198 var modalEl = modalId ? document.getElementById(modalId) : null; 199 var bsModal = (modalEl && window.bootstrap && window.bootstrap.Modal) 200 ? window.bootstrap.Modal.getOrCreateInstance(modalEl) 201 : null; 202 var $modal = modalEl ? $(modalEl) : null; 203 204 if ($modal) { 205 // Brightcove can't be re-pointed at a new video in place, so rebuild 206 // the holder on every close; resume the carousel we paused on open. 207 $modal.on('hide.bs.modal', function () { 208 $modal.find('.hp-hero__video-player').html('<div class="video-player-holder"></div>'); 209 if (hasMultiple) { 210 $track.trigger('play.owl.autoplay'); 211 } 212 }); 213 } 214 215 $section.on('click', '.hp-hero__play', function (e) { 216 e.preventDefault(); 217 var $a = $(this); 218 var videoId = $a.data('video-id-tmp'); 219 var playerId = $a.data('video-player-id-tmp'); 220 221 if (hasMultiple) { 222 $track.trigger('stop.owl.autoplay'); 223 } 224 225 // Desktop: play in the shared modal. Mobile: in-frame inline player. 226 if (bsModal && isDesktop()) {
227 $modal.find('.modal-title').text($a.data('video-title') || ''); 228 bsModal.show(); 229 window.loadSpecificVideo($modal.find('.video-player-holder'), videoId, playerId, '100%', '100%', true); 230 return; 231 } 232 233 var $frame = $a.closest('.hp-hero__media-frame'); 234 var $holder = $frame.find('.video-player-holder'); 235 236 $frame.css('background-image', 'none'); 237 $a.hide(); 238 $holder.removeClass('d-none'); 239 240 window.loadSpecificVideo($holder, videoId, playerId, '100%', '100%', true, false); 241 }); 242 } 243 } 244 245 /* ========================================================================= 246 * HP News - homepage "Latest News" carousel 247 * 248 * One owl carousel; each slide is a copy/image two-column grid. The 249 * prev/next + dots cluster (.hp-news__pagination) lives OUTSIDE the track 250 * (below content + image), so unlike the hero there is no reparenting - we 251 * only sync the active dot on every translate and proxy the nav clicks to 252 * owl. No carousel when there is a single article. 253 * 254 * No autoplay here: because the controls sit outside the track, they aren't 255 * covered by owl's autoplayHoverPause. With autoplay on, moving the pointer 256 * off the track to reach a button resumed autoplay and fired an extra 257 * advance - i.e. one click appeared to move the carousel 2-3 times. News is 258 * read-at-leisure content, so manual-only navigation is the right call. 259 * ========================================================================= */ 260 function initHpNews() { 261 if (typeof window.jQuery === 'undefined') { 262 return; 263 } 264 265 var $ = window.jQuery; 266 267 // querySelectorAll so every news section on the page is wired, not 268 // just the first one (querySelector stops at the first match). 269 document.querySelectorAll('[data-hp-news]').forEach(function (section, index) { 270 // Idempotency guard: never wire the same section twice. If init ran more 271 // than once, the delegated click handlers would stack and each physical 272 // click would advance the carousel multiple times. 273 if (section.hasAttribute('data-hp-news-ready')) { 274 return; 275 } 276 277 var $section = $(section); 278 var $track = $section.find('.hp-news__track').first(); 279 280 // Single article renders without .owl-carousel - nothing to wire up. 281 if (!$track.hasClass('owl-carousel') || typeof $.fn.owlCarousel !== 'function') { 282 return; 283 } 284 section.setAttribute('data-hp-news-ready', ''); 285 286 var $dots = $section.find('.hp-news__dot'); 287 288 var lockTrackWidth = function () { 289 $track.css('width', ''); 290 var naturalWidth = $track[0].getBoundingClientRect().width;
291 if (naturalWidth > 0) { 292 $track.css('width', Math.floor(naturalWidth) + 'px'); 293 } 294 }; 295 296 var syncDots = function () { 297 // owl's relative() converts the absolute (clone-inclusive) position 298 // to the real article index - the same reason the hero uses it: a 299 // swipe past the last slide can land on a clone. 300 var carousel = $track.data('owl.carousel'); 301 var realIndex; 302 if (carousel && typeof carousel.current === 'function') { 303 realIndex = carousel.relative(carousel.current()); 304 } else { 305 realIndex = 0; 306 } 307 308 if (typeof realIndex !== 'number' || realIndex < 0) { 309 return; 310 } 311 312 $dots.removeClass('is-active').attr('aria-pressed', 'false'); 313 $dots.filter('[data-slide-index="' + realIndex + '"]') 314 .addClass('is-active') 315 .attr('aria-pressed', 'true'); 316 }; 317 318 $track.on('initialized.owl.carousel translated.owl.carousel refreshed.owl.carousel resized.owl.carousel', syncDots); 319 320 lockTrackWidth(); 321 $track.owlCarousel({ 322 items: 1, 323 loop: true, 324 nav: false, 325 dots: false, 326 autoHeight: false 327 }); 328 329 syncDots(); 330 331 // The locked width is a fixed pixel value, so the track no longer 332 // follows the container and owl's own resize watcher sees no change. 333 // Re-lock and refresh ourselves (debounced like owl's 334 // responsiveRefreshRate). 335 // Use a per-instance namespace (index suffix) so a second news section 336 // doesn't remove the first section's resize handler when it binds its own. 337 var resizeTimer = null; 338 var resizeNs = 'resize.hpNews' + index; 339 $(window).off(resizeNs); 340 $(window).on(resizeNs, function () { 341 clearTimeout(resizeTimer); 342 resizeTimer = setTimeout(function () { 343 lockTrackWidth(); 344 $track.trigger('refresh.owl.carousel'); 345 }, 200); 346 }); 347 348 // Namespaced + de-duped so handlers can never stack (one click = one move). 349 $section.off('click.hpNews'); 350 $section.on('click.hpNews', '.hp-news__nav--prev', function (e) { 351 e.preventDefault(); 352 $track.trigger('prev.owl.carousel'); 353 }); 354 $section.on('click.hpNews', '.hp-news__nav--next', function (e) { 355 e.preventDefault(); 356 $track.trigger('next.owl.carousel'); 357 }); 358 $section.on('click.hpNews', '.hp-news__dot', function (e) { 359 e.preventDefault(); 360 var idx = parseInt(this.getAttribute('data-slide-index'), 10); 361 if (isNaN(idx)) { 362 return; 363 } 364 $track.trigger('to.owl.carousel', [idx, 300, true]); 365 }); 366 }); 367 } 368 369 /* ========================================================================= 370 * Featured Speakers - mobile carousel 371 * 372 * Desktop renders a static 4-up grid; the < 768px carousel is a separate 373 * .featured-speakers__track (one card per view) hidden on desktop via CSS. 374 * Same control model as HP News: custom prev/next + dots that sync the 375 * active dot on every translate (no autoplay, controls sit outside the 376 * track). owl.carousel JS ships via the legacy 2023/home-bundle. 377 * ========================================================================= */ 378 function initFeaturedSpeakers() { 379 if (typeof window.jQuery === 'undefined') { 380 return; 381 } 382 383 var $ = window.jQuery; 384 385 document.querySelectorAll('[data-featured-speakers]').forEach(function (section) {
386 // Idempotency guard: never wire the same section twice, or delegated 387 // click handlers would stack and one click would advance N times. 388 if (section.hasAttribute('data-featured-speakers-ready')) { 389 return; 390 } 391 392 var $section = $(section); 393 var $track = $section.find('.featured-speakers__track').first(); 394 395 // A single speaker renders without .owl-carousel - nothing to wire. 396 if (!$track.hasClass('owl-carousel') || typeof $.fn.owlCarousel !== 'function') { 397 return; 398 } 399 section.setAttribute('data-featured-speakers-ready', ''); 400 401 var $dots = $section.find('.featured-speakers__dot'); 402 403 var syncDots = function () { 404 // owl's relative() converts the absolute (clone-inclusive) 405 // position to the real speaker index - a swipe past the last 406 // slide can otherwise land on a clone. 407 var carousel = $track.data('owl.carousel'); 408 var realIndex; 409 if (carousel && typeof carousel.current === 'function') { 410 realIndex = carousel.relative(carousel.current()); 411 } else { 412 realIndex = 0; 413 } 414 415 if (typeof realIndex !== 'number' || realIndex < 0) { 416 return; 417 } 418 419 $dots.removeClass('is-active').attr('aria-pressed', 'false'); 420 $dots.filter('[data-slide-index="' + realIndex + '"]') 421 .addClass('is-active') 422 .attr('aria-pressed', 'true'); 423 }; 424 425 $track.on('initialized.owl.carousel translated.owl.carousel refreshed.owl.carousel', syncDots); 426 427 $track.owlCarousel({ 428 items: 1, 429 loop: true, 430 nav: false, 431 dots: false, 432 autoHeight: false 433 }); 434 435 syncDots(); 436 437 $section.off('click.featuredSpeakers'); 438 $section.on('click.featuredSpeakers', '.featured-speakers__nav--prev', function (e) { 439 e.preventDefault(); 440 $track.trigger('prev.owl.carousel'); 441 }); 442 $section.on('click.featuredSpeakers', '.featured-speakers__nav--next', function (e) { 443 e.preventDefault(); 444 $track.trigger('next.owl.carousel'); 445 }); 446 $section.on('click.featuredSpeakers', '.featured-speakers__dot', function (e) { 447 e.preventDefault(); 448 var idx = parseInt(this.getAttribute('data-slide-index'), 10); 449 if (isNaN(idx)) { 450 return; 451 } 452 $track.trigger('to.owl.carousel', [idx, 300, true]); 453 }); 454 }); 455 } 456 457 /* ========================================================================= 458 * Inner Testimonials - carousel navigation 459 * 460 * .inner-testimonials__track (the owl-carousel row) sits inside 461 * section.inner-testimonials. A custom .inner-testimonials__pagination row 462 * (prev chevron + dots + next chevron) lives below the carousel container. 463 * Same control model as HP News / Featured Speakers: own the owl init, sync 464 * the active dot on every translate, proxy nav clicks to owl. No autoplay. 465 * 466 * The block is deliberately NOT `section.testimonials`, so the legacy owl 467 * init in js/2023/{inner,home}.js (which targets `section.testimonials 468 * .owl-carousel`) never touches this element â this is the sole initialiser. 469 * ========================================================================= */ 470 function initTestimonialsCarousel() { 471 if (typeof window.jQuery === 'undefined') { 472 return; 473 } 474 475 var $ = window.jQuery; 476 477 document.querySelectorAll('section.inner-testimonials').forEach(function (section) {
478 // Idempotency guard: never wire the same section twice, or delegated 479 // click handlers would stack and one click would advance N times. 480 if (section.hasAttribute('data-testimonials-ready')) { 481 return; 482 } 483 484 var $section = $(section); 485 var $track = $section.find('.inner-testimonials__track').first(); 486 487 // A single testimonial renders without .owl-carousel - nothing to wire. 488 if (!$track.hasClass('owl-carousel') || typeof $.fn.owlCarousel !== 'function') { 489 return; 490 } 491 section.setAttribute('data-testimonials-ready', ''); 492 493 var $dots = $section.find('.inner-testimonials__dot'); 494 495 var syncDots = function () { 496 // owl's relative() converts the absolute (clone-inclusive) position 497 // to the real testimonial index - a swipe past the last slide can 498 // otherwise land on a clone. 499 var carousel = $track.data('owl.carousel'); 500 var realIndex; 501 if (carousel && typeof carousel.current === 'function') { 502 realIndex = carousel.relative(carousel.current()); 503 } else { 504 realIndex = 0; 505 } 506 507 if (typeof realIndex !== 'number' || realIndex < 0) { 508 return; 509 } 510 511 $dots.removeClass('is-active').attr('aria-pressed', 'false'); 512 $dots.filter('[data-slide-index="' + realIndex + '"]') 513 .addClass('is-active') 514 .attr('aria-pressed', 'true'); 515 516 // When the active slide has no image its quote is centred (CSS 517 // __item--no-media); flag the section so the shared controls centre 518 // to match at 992â1199px. Done in JS because the prod CSS minifier 519 // strips the equivalent :has() selector. 520 var $activeItem = $track.find('.owl-item.active').first() 521 .find('.inner-testimonials__item'); 522 section.classList.toggle( 523 'is-no-media-active', 524 $activeItem.hasClass('inner-testimonials__item--no-media') 525 ); 526 }; 527 528 $track.on('initialized.owl.carousel translated.owl.carousel refreshed.owl.carousel', syncDots); 529 530 $track.owlCarousel({ 531 items: 1, 532 loop: true, 533 nav: false, 534 dots: false, 535 autoHeight: false 536 }); 537 538 syncDots(); 539 540 $section.off('click.testimonials'); 541 $section.on('click.testimonials', '.inner-testimonials__nav--prev', function (e) { 542 e.preventDefault(); 543 $track.trigger('prev.owl.carousel'); 544 }); 545 $section.on('click.testimonials', '.inner-testimonials__nav--next', function (e) { 546 e.preventDefault(); 547 $track.trigger('next.owl.carousel'); 548 }); 549 $section.on('click.testimonials', '.inner-testimonials__dot', function (e) { 550 e.preventDefault(); 551 var idx = parseInt(this.getAttribute('data-slide-index'), 10); 552 if (isNaN(idx)) { 553 return; 554 } 555 $track.trigger('to.owl.carousel', [idx, 300, true]); 556 }); 557 }); 558 } 559 560 /* ========================================================================= 561 * Education Sessions 562 * 563 * The server embeds the full session pool as JSON inside a 564 * <script type="application/json" class="edu-sessions__pool-data"> element. 565 * On page load, 3 random sessions (sorted by date) are rendered into the 566 * .edu-sessions__cards grid. Clicking a tag filters the pool to sessions 567 * tagged for that interest/highlight and re-picks 3 random ones. 568 * The "More Sessions" button href is updated to include the active filter. 569 * ========================================================================= */ 570 function initEducationSessions() { 571 document.querySelectorAll('[data-edu-sessions]').forEach(function (section) { 572 if (section.hasAttribute('data-edu-sessions-ready')) { return; } 573 section.setAttribute('data-edu-sessions-ready', ''); 574 575 var poolEl = section.querySelector('.edu-sessions__pool-data'); 576 if (!poolEl) { return; } 577 578 var pool; 579 try { pool = JSON.parse(poolEl.textContent || '[]'); } 580 catch (e) { pool = []; } 581 582 var cardsEl = section.querySelector('.edu-sessions__cards'); 583 var moreBtn = section.querySelector('.edu-sessions__more-btn'); 584 var baseUrl = '/Search/EducationEvents.aspx'; 585 var sourceIncludes = (section.getAttribute('data-source-includes') || '').split(',').filter(
585Boolean); 586 var activeTags = []; // array of { type, id, name } 587 588 // Hide tags with zero matching sessions in the pool 589 section.querySelectorAll('.edu-sessions__tag').forEach(function (btn) { 590 var type = btn.getAttribute('data-filter-type'); 591 var id = parseInt(btn.getAttribute('data-filter-id'), 10); 592 var name = btn.getAttribute('data-filter-name') || ''; 593 var count = pool.filter(function (item) { 594 if (type === 'interest') { return item.interestIds && item.interestIds.indexOf(id) !== -1; } 595 if (type === 'highlight') { return item.highlightIds && item.highlightIds.indexOf(id) !== -1; } 596 if (type === 'sessiontype') { return item.sessionType === name; } 597 return false; 598 }).length; 599 if (count === 0) { btn.hidden = true; } 600 }); 601 602 // ------------------------------------------------------------------ 603 // Helpers 604 // ------------------------------------------------------------------ 605 function pickThree(subset) { 606 // Fisher-Yates shuffle, then take 3, then sort by sortKey. 607 var arr = subset.slice(); 608 for (var i = arr.length - 1; i > 0; i--) { 609 var j = Math.floor(Math.random() * (i + 1)); 610 var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; 611 } 612 return arr.slice(0, 3).sort(function (a, b) { 613 return a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0; 614 }); 615 } 616 617 function escHtml(str) { 618 return String(str || '') 619 .replace(/&/g, '&') 620 .replace(/</g, '<') 621 .replace(/>/g, '>') 622 .replace(/"/g, '"'); 623 } 624 625 function cardHtml(item) { 626 var authorHtml = ''; 627 if (item.speakerName) { 628 if (item.speakerUrl) { 629 authorHtml = '<div class="edu-sessions__author">' 630 + '<a href="' + escHtml(item.speakerUrl) + '" class="edu-sessions__author-link">' 631 + escHtml(item.speakerName) + '</a></div>'; 632 } else { 633 authorHtml = '<div class="edu-sessions__author">' 634 + '<span class="edu-sessions__author-name">' + escHtml(item.speakerName) + '</span></div>'; 635 } 636 } 637 638 var datetimeHtml = ''; 639 if (item.displayDate) { 640 datetimeHtml = '<p class="edu-sessions__datetime">' + escHtml(item.displayDate); 641 if (item.displayTime) { 642 datetimeHtml += '<br>' + escHtml(item.displayTime); 643 } 644 datetimeHtml += '</p>'; 645 } 646 647 var roomHtml = item.room 648 ? '<p class="edu-sessions__room">' + escHtml(item.room) + '</p>' 649 : ''; 650 651 var badgesHtml = ''; 652 if (item.isFeatured || item.requiresTicket || item.isExpoPass) { 653 badgesHtml = '<div class="edu-sessions__badges" aria-label="Session badges">'; 654 if (item.isFeatured) { 655 badgesHtml += '<span class="edu-sessions__badge edu-sessions__badge--featured" title="Featured Event" aria-label="Featured Event">' 656 + '<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="/svg/2027/icons.svg#icon-star-filled"></use></svg>' 657 + '</span>'; 658 } 659 if (item.requiresTicket) { 660 badgesHtml += '<span class="edu-sessions__badge edu-sessions__badge--ticket" title="Requires Ticket" aria-label="Requires Ticket">' 661 + '<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="/svg/2027/icons.svg#icon-dollar"></use></svg>' 662 + '</span>'; 663 } 664 if (item.isExpoPass) { 665 badgesHtml += '<span class="edu-sessions__badge edu-sessions__badge--pass" title="Expo+Education Pass" aria-label="Expo+Education Pass">' 666 + '<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="/svg/2027/icons.svg#icon-key"></use></svg>' 667 + '</span>'; 668 } 669 badgesHtml += '</div>'; 670 } else { 671 badgesHtml = '<span></span>'; 672 } 673 674 return '<div class="edu-sessions__card">' 675 + '<div class="edu-sessions__card-header"></div>' 676 + '<div class="edu-sessions__card-type">' 677 + '<span class="edu-sessions__type-label">' + escHtml(item.sessionType) + '</span>' 678 + '</div>' 679 + '<div class="edu-sessions__card-body">' 680 + '<h3 class="edu-sessions__title">' 681 + '<a href="' + escHtml(item.detailUrl) + '" class="edu-sessions__title-link">' 682 + escHtml(item.title)
683 + '</a>' 684 + '</h3>' 685 + authorHtml 686 + '<div class="edu-sessions__meta">' 687 + datetimeHtml + roomHtml 688 + '</div>' 689 + '<div class="edu-sessions__card-footer">' 690 + badgesHtml 691 + '<div class="edu-sessions__view-details-wrap">' 692 + '<a href="' + escHtml(item.detailUrl) + '" class="edu-sessions__view-details" target="_blank" rel="noopener noreferrer">View Details</a>' 693 + '</div>' 694 + '</div>' 695 + '</div>' 696 + '</div>'; 697 } 698 699 function renderCards(subset) { 700 if (!cardsEl) { return; } 701 var three = pickThree(subset); 702 if (!three.length) { 703 cardsEl.innerHTML = '<p class="edu-sessions__empty">No sessions found for this topic.</p>'; 704 return; 705 } 706 cardsEl.innerHTML = three.map(cardHtml).join(''); 707 } 708 709 function filterPool() { 710 if (!activeTags.length) { return pool; } 711 return pool.filter(function (item) { 712 return activeTags.some(function (tag) { 713 if (tag.type === 'interest') { 714 return item.interestIds && item.interestIds.indexOf(tag.id) !== -1; 715 } 716 if (tag.type === 'highlight') { 717 return item.highlightIds && item.highlightIds.indexOf(tag.id) !== -1; 718 } 719 if (tag.type === 'sessiontype') { 720 return item.sessionType === tag.name; 721 } 722 return false; 723 }); 724 }); 725 } 726 727 function updateMoreBtn() { 728 if (!moreBtn) { return; } 729 730 // Strip any existing hash so we don't generate an invalid URL like "...#...#...". 731 var url = baseUrl.split('#')[0]; 732 733 var params = []; 734 735 if (activeTags.length) { 736 // Only include source pools when a tag is active (keeps URL clean otherwise) 737 sourceIncludes.forEach(function (inc) { 738 if (inc) { params.push('include=' + encodeURIComponent(inc)); } 739 }); 740 741 activeTags.forEach(function (tag) { 742 if (tag.type === 'interest') { 743 params.push('interests=' + encodeURIComponent(tag.id)); 744 } else if (tag.type === 'highlight') { 745 // SignatureGroupTypeID maps to "signatureevent" on EducationEvents. 746 params.push('signatureevent=' + encodeURIComponent(tag.id)); 747 } else if (tag.type === 'sessiontype') { 748 if (tag.id > 0) { 749 // Cadmium session type â matches "include=ises_<id>" on EducationEvents 750 params.push('include=ises_' + encodeURIComponent(tag.id)); 751 } else if (tag.name === 'Pre-Show Course') { 752 params.push('include=preshow'); 753 } else if (tag.name === 'Special Event') { 754 params.push('include=edevents'); 755 } 756 } 757 }); 758 759 // De-dupe while preserving order. 760 params = params.filter(function (p, idx) { return params.indexOf(p) === idx; }); 761 } 762 763 moreBtn.href = url + (params.length ? '#' + params.join('&') : ''); 764 } 765 766 function syncPillStates() { 767 section.querySelectorAll('.edu-sessions__tag').forEach(function (t) { 768 var tType = t.getAttribute('data-filter-type'); 769 var tId = parseInt(t.getAttribute('data-filter-id'), 10); 770 var tName = t.getAttribute('data-filter-name') || ''; 771 var active = activeTags.some(function (tag) { 772 return tag.type === tType && tag.id === tId && tag.name === tN
772ame; 773 }); 774 t.classList.toggle('is-active', active); 775 t.setAttribute('aria-pressed', active ? 'true' : 'false'); 776 }); 777 } 778 779 // ------------------------------------------------------------------ 780 // Tag click 781 // ------------------------------------------------------------------ 782 section.addEventListener('click', function (e) { 783 var btn = e.target.closest('.edu-sessions__tag'); 784 if (!btn) { return; } 785 e.preventDefault(); 786 787 var type = btn.getAttribute('data-filter-type'); 788 var id = parseInt(btn.getAttribute('data-filter-id'), 10); 789 var name = btn.getAttribute('data-filter-name') || ''; 790 791 var existingIdx = -1; 792 for (var i = 0; i < activeTags.length; i++) { 793 if (activeTags[i].type === type && activeTags[i].id === id && activeTags[i].name === name) { 794 existingIdx = i; break; 795 } 796 } 797 if (existingIdx !== -1) { 798 activeTags.splice(existingIdx, 1); // deselect 799 } else { 800 activeTags.push({ type: type, id: id, name: name }); // add 801 } 802 803 syncPillStates(); 804 renderCards(filterPool()); 805 updateMoreBtn(); 806 }); 807 808 // Initialise More Sessions href with source-based include params. 809 updateMoreBtn(); 810 }); 811 } 812})(); 813 814/* ============================================================================= 815 * HP Videos - mobile carousel 816 * Initialises the owl carousel on #video_mobile for â¤767px viewports. 817 * owl.carousel JS ships via legacy bundles (inner-bundle / home-bundle). 818 * ============================================================================= */ 819(function () { 820 'use strict'; 821 822 function initHpVideos() { 823 var $carousel = $('#video_mobile .owl-carousel'); 824 if (!$carousel.length) { 825 return; 826 } 827 if (typeof $.fn.owlCarousel !== 'function') { 828 return; 829 } 830 $carousel.owlCarousel({ 831 items: 1, 832 loop: false, 833 nav: true, 834 dots: true, 835 navText: ['‹', '›'], 836 responsive: { 837 0: { items: 1 } 838 } 839 }); 840 } 841 842 function onReady(callback) { 843 if (document.readyState === 'complete') { 844 callback(); 845 return; 846 } 847 document.addEventListener('DOMContentLoaded', callback); 848 } 849 850 onReady(function () { 851 initHpVideos(); 852 }); 853})(); 854 855/* ============================================================================= 856 * Inner Top List - desktop pills + mobile accordion 857 * Desktop: clicking a pill activates it and shows the matching right-column panel. 858 * Mobile: clicking an accordion trigger toggles its panel open/closed. 859 * Item 1 starts active on both surfaces (set in Razor markup). 860 * ============================================================================= */ 861(function () { 862 'use strict'; 863 864 function initInnerTopList() { 865 var sections = document.querySelectorAll('.inner-top-list'); 866 if (!sections.length) { return; } 867 868 sections.forEach(function (section) { 869 var sectionId = section.getAttribute('data-section-id') || ''; 870 var storageKey = 'itl-active-' + sectionId; 871 872 function saveActive(index) { 873 if (!sectionId) { return; } 874 try { sessionStorage.setItem(storageKey, index); } catch (e) {} 875 } 876 877 // ââ Desktop: pill list + right-column panel âââââââââââââââââââââââââââââââââ 878 var list = section.querySelector('.inner-top-list__items'); 879 if (list) { 880 var items = Array.prototype.slice.call(list.querySelectorAll('.inner-top-list__item')); 881 var panelItems = Array.prototype.slice.call(section.querySelectorAll('.inner-top-list__panel-item')); 882 883 function activateIndex(index) {
884 items.forEach(function (el) { 885 el.classList.remove('inner-top-list__item--active'); 886 }); 887 panelItems.forEach(function (el) { 888 el.classList.remove('inner-top-list__panel-item--active'); 889 }); 890 items[index].classList.add('inner-top-list__item--active'); 891 if (panelItems[index]) { 892 panelItems[index].classList.add('inner-top-list__panel-item--active'); 893 } 894 saveActive(index); 895 } 896 897 items.forEach(function (item, index) { 898 item.addEventListener('click', function () { 899 activateIndex(index); 900 }); 901 }); 902 903 // Restore saved active item (e.g. after browser back) 904 var saved = sectionId ? (function () { try { return sessionStorage.getItem(storageKey); } catch (e) { return null; } }()) : null; 905 if (saved !== null) { activateIndex(Math.min(parseInt(saved, 10), items.length - 1)); } 906 } 907 908 // ââ Mobile: accordion ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 909 var accItems = Array.prototype.slice.call(section.querySelectorAll('.itl-accordion__item')); 910 accItems.forEach(function (accItem, accIndex) { 911 var trigger = accItem.querySelector('.itl-accordion__trigger'); 912 var panel = accItem.querySelector('.itl-accordion__panel'); 913 if (!trigger || !panel) { return; } 914 915 trigger.addEventListener('click', function () { 916 var isOpen = accItem.classList.contains('itl-accordion__item--active'); 917 918 // Collapse all items 919 accItems.forEach(function (el) { 920 el.classList.remove('itl-accordion__item--active'); 921 var t = el.querySelector('.itl-accordion__trigger'); 922 var p = el.querySelector('.itl-accordion__panel'); 923 if (t) { t.setAttribute('aria-expanded', 'false'); } 924 if (p) { p.setAttribute('hidden', ''); } 925 }); 926 927 // Expand clicked item if it was collapsed 928 if (!isOpen) { 929 accItem.classList.add('itl-accordion__item--active'); 930 trigger.setAttribute('aria-expanded', 'true'); 931 panel.removeAttribute('hidden'); 932 saveActive(accIndex); 933 } 934 }); 935 }); 936 937 // Restore accordion state on back navigation 938 var savedAcc = sectionId ? (function () { try { return sessionStorage.getItem(storageKey); } catch (e) { return null; } }()) : null; 939 if (savedAcc !== null && accItems.length) { 940 var accIdx = Math.min(parseInt(savedAcc, 10), accItems.length - 1); 941 if (accIdx > 0) { 942 accItems[0].classList.remove('itl-accordion__item--active'); 943 var t0 = accItems[0].querySelector('.itl-accordion__trigger'); 944 var p0 = accItems[0].querySelector('.itl-accordion__panel'); 945 if (t0) { t0.setAttribute('aria-expanded', 'false'); } 946 if (p0) { p0.setAttribute('hidden', ''); } 947 accItems[accIdx].classList.add('itl-accordion__item--active'); 948 var ta = accItems[accIdx].querySelector('.itl-accordion__trigger'); 949 var pa = accItems[accIdx].querySelector('.itl-accordion__panel'); 950 if (ta) { ta.setAttribute('aria-expanded', 'true'); } 951 if (pa) { pa.removeAttribute('hidden'); } 952 } 953 } 954 }); 955 } 956 957 function onReady(callback) { 958 if (document.readyState === 'complete' || document.readyState === 'interactive') { 959 callback(); 960 return; 961 } 962 document.addEventListener('DOMContentLoaded', callback); 963 } 964 965 onReady(function () { 966 initInnerTopList(); 967 });
968}());
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.