PageSourceSearch

https://www.buildersshow.com/js/2027/sections-bundle.js?v=Zw4hDpos3cuz4_tXC4CtiXYjWdbLZwoWCFz-DpV04Zw

js buildersshow.com collected 2026-10-02 05:05:24 UTC 45,715 bytes, 968 lines download raw bytes

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, '&amp;')
620                    .replace(/</g, '&lt;')
621                    .replace(/>/g, '&gt;')
622                    .replace(/"/g, '&quot;');
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: ['&#8249;', '&#8250;'],
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.