PageSourceSearch

https://www.umontreal.ca/typo3conf/ext/udem_www/Resources/Public/udem_www_main.js?1769692477

js umontreal.ca collected 2026-10-01 07:33:50 UTC 86,550 bytes, 2,300 lines download raw bytes

1
2/* global udem_www, localforage, AOS, _ */
3
4// Global namespace, for utilies and shared state.
5if (typeof window.udem_www === 'undefined') {
6    window.udem_www = {};
7}
8
9// TODO harden or remove underscore/lowdash dependency
10if (typeof _ === 'undefined') {
11    console.log('ERROR! Missing "Underscore/lowdash" library.');
12}
13udem_www.debounce = _.debounce;
14udem_www.groupBy = _.groupBy;
15udem_www.find = _.find;
16
17// Fixes scrolling position when navigating to content anchor '#c[0-9]+'. Default delay is 160ms.
18udem_www.scrollToAnchoredContent = function (delay) {
19    'use strict';
20    var $ = jQuery,
21        hash = window.location.href.match(/(#c[0-9]+)|(#[-a-z0-9]+)$/),
22        $content;
23
24    delay = typeof delay === 'number' ? delay : 160;
25
26    if (hash && hash.length) {
27        hash = hash[0];
28        $content = $(hash).first();
29
30        // Simple anchors don't report a precise position (in webkit at least)
31        if (hash[2] && $content.is('a[id]')) {
32            // $content = $content.parents('[id^="c"]');
33            $content = $content.parents(':visible');
34        }
35
36        // console.log('scrollToAnchoredContent hash / $content: ', hash, $content);
37        if ($content.length) {
38            setTimeout(function () { udem_www.scrollToElement($content, $('#udemwww-main-toolbar').height()); }, delay);
39        }
40    }
41};
42
43// Scrolls the viewport to an element top position, minus the given offset.
44udem_www.scrollToElement = function (element, offset) {
45    'use strict';
46    var $ = jQuery,
47        $element = $(element),
48        element_offset;
49
50    if ($element.length) {
51        element_offset = $element.offset();
52        if (typeof offset === 'undefined') {
53            offset = 0;
54        }
55        // console.log('element_offset.top, offset: ', element_offset.top, offset);
56        if (element_offset.top > offset) {
57            $('html').scrollTop(element_offset.top - offset); // IE
58            $('body').scrollTop(element_offset.top - offset); // Gecko/Webkit/...
59        }
60    }
61};
62
63/**
64 * Return if the search window is shown.
65 * @returns {Boolean}
66 */
67udem_www.search_is_shown = function () {
68    return $('body').hasClass('udem-show-search-modal');
69};
70
71/**
72 * Check if the browser use an inner scrollbar (Ex Edge and mobile devices, not Chrome or Firefox).
73 * @returns {Boolean}
74 */
75udem_www.use_inner_scrollbar = (function () {
76    let use_inner_scrollbar_cached;
77    return function () {
78        if (typeof use_inner_scrollbar_cached === 'undefined') {
79            var $body = $('body'),
80                originalStyle = $body.attr('style'),
81                scrollWidthWithScroll,
82                scrollWidthWithoutScroll;
83            $body.css('overflow', 'scroll');
84            scrollWidthWithScroll = window.innerWidth > document.documentElement.clientWidth;
85            $body.css('overflow', 'hidden');
86            scrollWidthWithoutScroll = window.innerWidth > document.documentElement.clientWidth;
87            if (typeof originalStyle !== 'undefined') {
88                $body.attr('style', originalStyle);
89            } else {
90                $body.removeAttr('style');
91            }
92            use_inner_scrollbar_cached = scrollWidthWithScroll === scrollWidthWithoutScroll;
93        }
94        return use_inner_scrollbar_cached;
95    };
96}());
97
98/**
99 * Get the scrollbar width
100 * @returns {Number}
101 */
102udem_www.get_scrollbar_width = (function () {
103    let get_scrollbar_width_cached;
104    return function () {
105        if (typeof get_scrollbar_width_cached === 'undefined') {
106            var width = 100,
107                $outer = $('<div>').css({ visibility: 'hidden', width: width, overflow: 'scroll' }).appendTo('body'),
108                widthWithScroll = $('<div>').css({ width: '100%' }).appendTo($outer).outerWidth();
109            $outer.remove();
110            get_scrollbar_width_cached = width - widthWithScroll;
111        }
112        return get_scrollbar_width_cached;
113    };
114}());
115
116/**
117 * Position the main toolbar based on scrollbar.
118 * Used to keep in place the main toolbar's buttons, even if there a scrollbar or not on the body.
119 */
120udem_www.position_main_toolbar = function () {
121    if (!udem_www.use_inner_scrollbar()) {
122        var hasScrollbar = window.innerWidth > document.documentElement.clientWidth,
123            width = udem_www.get_scrollbar_width(),
124            $connectionMenu = $('#udem-menu-connexion');
125        $('#udemwww-main-toolbar-container').css('right', hasScrollbar ? 0 : width);
126        $('#udemwww-main-toolbar > div > h1 > a').css('transform', hasScrollbar ? 'none' : 'translateX(-' + (width / 2) + 'px)');
127        if ($connectionMenu.css('position') === 'absolute') {
128            $connectionMenu.css('transform', hasScrollbar ? 'none' : 'translateX(-' + (width / 2) + 'px)');
129        }
130    }
131};
132
133/*
134 * The body will have the class "udem-urgence-active" if the urgence plugin triggered the
135 * onUdemUrgence custom event
136 */
137window.addEventListener('onUdemUrgence', function (e) {
138    var bannerSizeClass = 'udem-urgence-active-small';
139    if ($('#udem-urgence-bcrp').hasClass('large')) {
140        bannerSizeClass = 'udem-urgence-active-large';
141    }
142    $('body').addClass('udem-urgence-active ' + bannerSizeClass);
143}, false);
144
145// The body will have the class "udemwww-scrolled" if the viewport is vertically scrolled by more than 16px from the top.
146// Depending on the browser, the value may be updated during the scroll, or at the end of the scroll.
147// The Urgence banner will also be reduced
148udem_www.update_body_scrolled = function () {
149    'use strict';
150    var scrollY = (typeof window.pageYOffset === 'undefined')
151        ? (document.documentElement || document.body.parentNode || document.body).scrollTop
152        : window.pageYOffset;
153    jQuery('body').toggleClass('udemwww-scrolled', scrollY >= 16);
154
155    if ($('body').hasClass('udem-urgence-active') || $('body').hasClass('udem-urgence-muted')) {
156        if (scrollY >= 1) {
157            $('body').removeClass('udem-urgence-active');
158            $('#udem-urgence-bcrp').hide();
159            $('body').addClass('udem-urgence-muted');
160        } else {
161            $('body').removeClass('udem-urgence-muted');
162            $('#udem-urgence-bcrp').show();
163            $('body').addClass('udem-urgence-active');
164        }
165    }
166};
167
168jQuery(function ($) {
169    'use strict';
170
171    // Init update_body_scrolled
172    $(window).on('scroll', udem_www.debounce(udem_www.update_body_scrolled, 16));
173    setTimeout(udem_www.update_body_scrolled, 16);
174
175    // Init scrollToAnchoredContent.
176    $(window).on('hashchange', udem_www.scrollToAnchoredContent);
177
178    // Wait for images be loaded, otherwize offset would be not accurate.
179    if (document.readyState === 'complete') {
180        udem_www.scrollToAnchoredContent();
181    } else {
182        $(window).one('load', udem_www.scrollToAnchoredContent);
183    }
184});
185
186// Main menu
187jQuery(function ($) {
188    'use strict';
189    var $mainmenu_menu = $('#udemwww-mainmenu'),
190        $mainmenu_trigger = $('#udemwww-mainmenu-trigger'),
191        $mainmenu_overlay = $('#udemwww-mainmenu-overlay'),
192        mainmenu_cls = 'udemwww-show-mainmenu';
193
194    // Creates (once) the menus overlay
195    function mainmenu_init_overlay() {
196        if ($mainmenu_overlay.length === 0) {
197            $mainmenu_overlay = $('<div id="udemwww-mainmenu-overlay"></div>').appendTo(document.body);
198        }
199    }
200
201    // TODO May be more robust to have persitent "click" handler on the body (to avoid the "re-init")...
202    function mainmenu_init_close() {
203        $('body').one('click', function (event) {
204            if (mainmenu_is_shown()) {
205                if ($.contains($mainmenu_menu.get(0), event.target)) {
206                    // Clicked a link in menu, which may be an anchor in the same page?
207                    var clicked_link_href = $(event.target).closest('a[href]').prop('href');
208                    if (clicked_link_href && clicked_link_href.replace(/#.*$/, '#') === window.location.href.replace(/#.*$/, '') + '#') {
209                        // hide but do not prevent default!
210                        mainmenu_hide();
211                        udem_www.scrollToAnchoredContent();
212                    } else {
213                        // re-init!
214                        mainmenu_init_close();
215                    }
216                } else {
217                    event.preventDefault();
218                    event.stopPropagation();
219                    mainmenu_hide();
220                }
221                udem_www.position_main_toolbar();
222            }
223        });
224    }
225
226    function mainmenu_is_shown() {
227        return $('body').hasClass(mainmenu_cls);
228    }
229
230    function mainmenu_hide() {
231        $('body').removeClass(mainmenu_cls);
232        $mainmenu_menu.removeClass('udemwww-mainmenu-opened');
233    }
234
235    function mainmenu_show() {
236        if (!mainmenu_is_shown()) {
237            mainmenu_init_overlay();
238            $('body').addClass(mainmenu_cls);
239            mainmenu_init_close();
240
241            $mainmenu_menu.one('transitionend', function (event) {
242                $mainmenu_menu.addClass('udemwww-mainmenu-opened');
243            });
244
245            // Also close the search modal!
246            // TODO but the search-modal state should be also fixed!
247            $('body').removeClass('udem-show-search-modal');
248            udem_www.position_main_toolbar();
249        }
250    }
251
252    $mainmenu_trigger.on('click', function (event) {
253        if (!mainmenu_is_shown()) {
254            event.preventDefault();
255            event.stopPropagation();
256            mainmenu_show();
257            udem_www.position_main_toolbar();
258        }
259    });
260});
261
262udem_www.resize_custom_menu_container = function () {
263    $('.udemwww-level2-custom-container').each(function() {
264        var $this = $(this),
265            $mainmenu_menu = $('#udemwww-mainmenu'),
266            $container = $this.parent(),
267            $height = '';
268        $this.css('height', $height);
269        if ($mainmenu_menu.css('position') === 'static' && $container.parent().hasClass('expanded')) {
270            $height = $container.innerHeight() -
271            parseInt($this.css('marginTop')) -
272            parseInt($this.css('marginBottom')) + 'px';
273        }
274        $this.css('height', $height);
275    });
276};
277
278udem_www.fix_primarynav_expanded_position = function () {
279    'use strict';
280    var $item = $('#udemwww-primarynav > ul > li.expandable.expanded > a'),
281        $div = $item.length ? $item.next() : [],
282        $li, $ul, ul_offset, ul_width, li_offset,
283        li_width, li_relative_offset, fix_offset,
284        div_width;
285
286    if ($div.length && $div.css('position') === 'absolute') {
287        // We want the largest div_width possible, with "natural" layout...
288        $li = $item.parent();
289        $ul = $li.parent();
290        ul_offset = $ul.offset().left;
291        ul_width = $ul.width();
292        li_offset = $li.offset().left;
293        li_width = $li.width();
294        li_relative_offset = li_offset - ul_offset;
295        fix_offset = 0;
296        div_width = 0;
297
298        $div.css({ left: 0, maxWidth: ul_width + 'px' });
299        div_width = $div.width();
300        $div.css({ left: 'auto' });
301
302        fix_offset = ((div_width - li_width) / -2) - 1;
303
304        if (li_relative_offset + div_width + fix_offset > ul_width) {
305            fix_offset = ul_width - li_relative_offset - div_width - 3;
306        }
307
308        if (li_relative_offset < -fix_offset) {
309            fix_offset = -li_relative_offset - 1;
310        }
311
312        $div.css({
313            marginLeft: Math.round(fix_offset) + 'px'
314        });
315    } else {
316        $('#udemwww-primarynav > ul > li.expandable > div').css({
317            marginLeft: '0',
318            maxWidth: 'none'
319        });
320    }
321};
322
323udem_www.getHeight = function (selector) {
324    'use strict';
325    var $elements = jQuery(selector),
326        height = 0;
327
328    if ($elements.length === 0) {
329        return 0;
330    }
331
332    height = Math.max($elements.first().height(), typeof $elements[0].clientHeight !== 'undefined' ? $elements[0].clientHeight : 0);
333    return height;
334};
335
336udem_www.set_columns_for_menus = function() {
337    var max_items_per_columns = $('#udemwww-primarynav').data('max-items-per-column');
338    if (!max_items_per_columns) {
339        return;
340    }
341    $('#udemwww-primarynav > ul > li.expandable > div> ul').each(function() {
342        var $this = $(this),
343            nb_items = $this.find('> li').length,
344            nb_columns = Math.ceil(nb_items / max_items_per_columns);
345        if (nb_columns > 1) {
346            $this.addClass('udem-menu-' + nb_columns + '-columns');
347        }
348    });
349}
350
351jQuery(function ($) {
352    'use strict';
353    $(window).on('resize scroll scrollstop', udem_www.debounce(udem_www.fix_primarynav_expanded_position, 16));
354
355    // Fix the mega menu custom container height to fit its parent (only for large display and non scrollable).
356    $(window).on('resize scroll scrollstop', udem_www.debounce(udem_www.resize_custom_menu_container, 16));
357
358    // Fix the number of columns for each list if the number of item exceed the maximun allowed.
359    udem_www.set_columns_for_menus();
360});
361
362// Menu with expandable submenus
363jQuery(function ($) {
364    'use strict';
365    var $body = $('body'),
366        $mainmenu_menu = $('#udemwww-mainmenu'),
367        $nav_with_expandables = $('#udemwww-primarynav, #udem-menu-connexion, #udem-menu-liens-rapides, #udem-menu-clienteles');
368
369    $nav_with_expandables.on('click', '> ul > li.expandable > a', function (event) {
370        var $item = $(this),
371            $li = $item.parent(),
372            $ul = $li.parent(),
373            $nav = $ul.parent(),
374            expand = !$li.hasClass('expanded');
375
376        event.preventDefault();
377
378        if ($nav.is('#udem-menu-connexion')) {
379            $('#udem-menu-connexion-trigger').toggleClass('active', expand);
380        }
381
382        // Close all expanded
383        $nav_with_expandables.find('> ul > li.expandable.expanded').removeClass('expanded');
384
385        if (expand) {
386            if ($li.css('position') !== 'static') {
387                $mainmenu_menu.animate({scrollTop: $li.offset().top - $mainmenu_menu.offset().top + $mainmenu_menu.scrollTop() -1}, 300);
388            }
389            $li.addClass('expanded');
390        }
391
392        udem_www.fix_primarynav_expanded_position();
393        udem_www.resize_custom_menu_container();
394    });
395
396    // Create a return bouton for each expandable content (mobile and scrollable only).
397    $nav_with_expandables.filter(":not(#udem-menu-clienteles)").each(function() {
398        $(this).find('ul > li.expandable').each(function() {
399            var $this = $(this),
400                $title = $this.children(":first"),
401                $container = $this.children("div"),
402                $close_menu = $title.clone().prependTo($container);
403            $close_menu.removeClass().addClass('udem-close-menu');
404            $close_menu.on('click', function(e) {
405                e.preventDefault();
406                $this.removeClass('expanded');
407                $('#udem-menu-connexion-trigger').removeClass('active');
408            });
409        });
410
411    });
412
413    // Special case: #udem-menu-connexion on non-scrolled-desktop is a dropdown.
414    $('#udem-menu-connexion-trigger').on('click', function (event) {
415        var $trigger = $(this),
416            $nav = $('#udem-menu-connexion'),
417            $li = $nav.find('>ul>li'),
418            expand = !$li.hasClass('expanded');
419
420        event.preventDefault();
421
422        if ($nav.css('position') === 'absolute') {
423            event.stopPropagation();
424        }
425
426        // Close all expanded
427        $nav_with_expandables.find('> ul > li.expandable.expanded').removeClass('expanded');
428
429        if ($('#udem-menu-tiroir').hasClass('udem-menu-tiroir-ouvert')) {
430            udem_www.menu_tiroir.close();
431        }
432
433        $trigger.toggleClass('active', expand);
434        if (expand) {
435            $li.addClass('expanded');
436        }
437
438        udem_www.fix_primarynav_expanded_position();
439    });
440
441    // Special case: #udem-menu-connexion on desktop is a dropdown. So we may want to close it at some point!
442    $body.on('click', function (event) {
443        // Trigger click if its the lang menu or udem-je-donne.
444        if (event.target.id === 'udem-toolbar-menu-langues' || $(event.target).hasClass('udem-je-donne')) {
445            $(event.target).trigger('click');
446        }
447
448        var $expanded = $('#udem-menu-connexion .expandable.expanded');
449        if ($expanded.length > 0 && $('#udem-menu-connexion').css('position') === 'absolute') {
450            $expanded.removeClass('expanded');
451            $('#udem-menu-connexion-trigger').removeClass('active');
452        }
453    });
454
455    // Prevent the propagation on the lang menu and udem-je-donne.
456    $('#udem-toolbar-menu-langues, .udem-je-donne').on('click', function (event) {
457        event.stopPropagation();
458    });
459
460    if ($('#udemwww-main-toolbar .udem-je-donne').length) {
461        $body.addClass('with-udem-je-donne');
462    }
463});
464
465// Menu tiroir
466udem_www.menu_tiroir = {
467    styleElement: null
468};
469
470// Updates the dedicated style element
471udem_www.menu_tiroir.updateStyleSheet = function (css) {
472    var styleElement = udem_www.menu_tiroir.styleElement;
473    try {
474        if (!styleElement) {
475            styleElement = document.createElement('style');
476            styleElement.setAttribute('type', 'text/css');
477            styleElement.setAttribute('id', 'udemwww-menu-tiroir-style');
478            (document.head || document.getElementsByTagName('head')[0]).appendChild(styleElement);
479
480            udem_www.menu_tiroir.styleElement = styleElement;
481        }
482
483        if (styleElement.styleSheet) {
484            styleElement.styleSheet.cssText = css; // Old IE
485        } else {
486            for (let i = 0; i < styleElement.childNodes.length; i++) {
487                styleElement.removeChild(styleElement.childNodes[i]);
488            }
489            styleElement.appendChild(document.createTextNode(css));
490        }
491    } catch (e) {
492        console.log('Could not update styleSheet: ', e, this, arguments);
493    }
494};
495
496udem_www.menu_tiroir.open = function () {
497    'use strict';
498    var $ = jQuery,
499        $body = $('body'),
500        $trigger = $('#udem-menu-tiroir-trigger'),
501        $tiroir = $('#udem-menu-tiroir'),
502        $expandable = $tiroir.find('.expandable').first(),
503        tiroir_height,
504        toolbar_top,
505        // FIXME use the same value than @udem-main-toolbar-extralarge-bordertop-size less variable.
506        border_top = 4;
507
508    // Close all expanded expandables
509    // FIXME selectors should not be hardcoded here!
510    $('#udemwww-primarynav, #udem-menu-connexion, #udem-menu-liens-rapides, #udem-menu-clienteles').find('> ul > li.expandable.expanded').removeClass('expanded');
511    $('#udem-menu-connexion-trigger').removeClass('active');
512
513    $trigger.addClass('active');
514    $tiroir.addClass('udem-menu-tiroir-ouvert');
515    $tiroir.addClass('udem-menu-tiroir-entrouvert');
516    $body.addClass('udem-menu-tiroir-animated');
517    $expandable.addClass('expanded');
518    $body.addClass('udem-show-menu-tiroir');
519
520    tiroir_height = udem_www.getHeight('#udem-menu-tiroir-inner');
521    toolbar_top = $('#udemwww-main-toolbar').offset().top;
522
523    udem_www.menu_tiroir.updateStyleSheet([
524        'body:not(.udemwww-scrolled).udem-show-menu-tiroir #udemwww-main-toolbar { margin-top: ' +
525            (tiroir_height + toolbar_top - border_top) + 'px; }'
526    ].join('\n'));
527};
528
529/**
530 * Remove classes at the end of the tiroir closing.
531 * @param {jQuery} $body
532 * @param {jQuery} $expandable
533 * @param {jQuery} $tiroir
534 */
535udem_www.menu_tiroir.close_end = function ($body, $expandable, $tiroir) {
536    $body.removeClass('udem-show-menu-tiroir');
537    $expandable.removeClass('expanded');
538    $tiroir.removeClass('udem-menu-tiroir-ouvert');
539    $body.removeClass('udem-menu-tiroir-animated');
540};
541
542/**
543 *
544 * @param {Boolean} animate
545 * @returns {undefined}
546 */
547udem_www.menu_tiroir.close = function (animate) {
548    'use strict';
549    var $ = jQuery,
550        $body = $('body'),
551        $trigger = $('#udem-menu-tiroir-trigger'),
552        $tiroir = $('#udem-menu-tiroir'),
553        $expandable = $tiroir.find('.expandable').first();
554
555    animate = typeof animate !== 'undefined' ? animate : false;
556
557    if (animate) {
558        // Remove classes at the end of the css animation when animated.
559        $tiroir.one('transitionend', function (event) {
560            udem_www.menu_tiroir.close_end($body, $expandable, $tiroir);
561        });
562    } else {
563        udem_www.menu_tiroir.close_end($body, $expandable, $tiroir);
564    }
565
566    $trigger.removeClass('active');
567    $body.toggleClass('udem-menu-tiroir-animated', animate);
568    $tiroir.removeClass('udem-menu-tiroir-entrouvert');
569
570    udem_www.menu_tiroir.updateStyleSheet('');
571};
572
573// Search modal and menu tiroir
574jQuery(function ($) {
575    'use strict';
576    var lang = $('html').attr('lang') === 'en' ? 'en' : 'fr',
577        $body = $('body'),
578        $search_trigger = $('#udem-search-trigger'),
579        $search_modal = $('<div id="udemwww-search-modal"></div>').appendTo(document.body),
580
581        search_content_loaded = false,
582        search_personne_form_initialized = false,
583
584        gcse_initialized = false,
585        gcse_poll_interval = 300, // milliseconds
586        gcse_poll_max_calls = 20, // max. 20 tries
587        gcse_poll_counter = 0,
588
589        max_reached_msg = 'Résultats : Le maximum de ${n} personnes affichées est atteint. Nous vous recommandons d’affiner vos critères de recherche.';
590
591    function ll(msg, vars) {
592        /* eslint-disable no-template-curly-in-string */
593        if (lang === 'en') {
594            switch (msg) {
595            case 'Recherche en cours':
596                msg = 'Searching...';
597                break;
598            case 'Aucun résultat':
599                msg = 'No results';
600                break;
601            case 'Résultats : 1 personne trouvée':
602                msg = 'Result: 1 person found';
603                break;
604            case max_reached_msg:
605                msg = 'Results: The maximum of ${n} people displayed has been reached. We recommend that you refine your search criteria.';
606                break;
607            case 'Résultats : ${n} personnes trouvées':
608                msg = 'Results: ${n} persons found';
609                break;
610            case 'Une erreur est survenue durant la recherche':
611                msg = 'An error has occurred during the search';
612                break;
613            }
614        }
615        /* eslint-enable no-template-curly-in-string */
616
617        // Poor Man templates...
618        if (typeof vars === 'object') {
619            $.each(vars, function (k, v) {
620                msg = msg.replace('${' + k + '}', v);
621            });
622        }
623
624        return msg;
625    }
626
627    // Called when gcse is ready (input is available).
628    function gcse_onready() {
629        // $('#udemwww-search-modal form.gsc-search-box input.gsc-input').trigger('focus');
630    }
631
632    // Polls for gcse readyness (input is available), then call gcse_onready().
633    function gcse_poll_ready() {
634        if ($search_modal.find('form.gsc-search-box').length > 0) {
635            gcse_poll_counter = 0;
636            gcse_onready();
637        } else if (gcse_poll_counter < gcse_poll_max_calls) {
638            gcse_poll_counter++;
639            setTimeout(gcse_poll_ready, gcse_poll_interval);
640        } else {
641            console.log('gcse_poll limit reached: ', gcse_poll_counter, gcse_poll_max_calls);
642            // TODO display error message to user?
643        }
644    }
645
646    // Initializes gcse.
647    function gcse_init() {
648        setTimeout(gcse_poll_ready, gcse_poll_interval);
649
650        if (gcse_initialized) {
651            return;
652        }
653
654        // TODO put cx in an html data attribute!
655        var cx = '011034801931486537785:va21ny36mjw',
656            gcse = document.createElement('script'),
657            s = document.getElementsByTagName('script')[0];
658        gcse.type = 'text/javascript';
659        gcse.async = true;
660        gcse.src = 'https://cse.google.com/cse.js?cx=' + cx;
661        s.parentNode.insertBefore(gcse, s);
662    }
663
664    function search_init_tabs() {
665        var $search_tabbar_items = $search_modal.find('.search-tabbar a'),
666            $search_tabs_items = $search_modal.find('.search-tabs > *');
667
668        $search_tabbar_items.on('click', function (event) {
669            var href = $(this).attr('href'),
670                $tab = [];
671
672            if (href.match(/^#[-_.a-zA-Z0-9]+$/)) {
673                $tab = $search_tabs_items.filter(href);
674            }
675
676            if ($tab.length > 0) {
677                event.preventDefault();
678
679                $search_tabbar_items.removeClass('active');
680                $search_tabs_items.hide();
681
682                $(this).addClass('active');
683                $tab.show();
684
685                // Update the location hash
686                window.location.hash = href;
687
688                $tab.find('form input[type=search], form input[type=text]').first().trigger('focus');
689            }
690        });
691    }
692
693    function search_load_content(callback) {
694        if (search_content_loaded) {
695            if (typeof callback === 'function') {
696                callback();
697            }
698            return;
699        }
700
701        var url = $search_trigger.attr('data-search-modal-url');
702        if (url) {
703            $search_modal.load(url, function () {
704                if (typeof callback === 'function') {
705                    search_content_loaded = true;
706                    callback();
707                }
708            });
709        } else {
710            console.log('COULD NOT FIND data-search-modal-url!');
711        }
712    }
713
714    function search_hide() {
715        $body.removeClass('udem-show-search-modal');
716        udem_www.position_main_toolbar();
717
718        // Remove the #udemwww-search-* location hash
719        if (typeof window.location.hash.replace === 'function') {
720            window.location.hash = window.location.hash.replace(
721                /^(#udemwww-search-sites|#udemwww-search-programme|#udemwww-search-personne|#udemwww-search-sofia)(&.*)?$/,
722                ''
723            );
724        }
725    }
726
727    // TODO May be more robust to have persistent "click" handler on the body...
728    function search_init_close() {
729        $body.one('click', function (event) {
730            if (!jQuery.contains($search_modal.get(0), event.target) && event.target !== $search_modal.get(0)) {
731                event.preventDefault();
732                search_hide();
733            } else {
734                // reinit!
735                search_init_close();
736            }
737        });
738    }
739
740    function search_personne_form_init() {
741        if (search_personne_form_initialized) {
742            return;
743        }
744        search_personne_form_initialized = true;
745
746        $search_modal.find('#udemwww-search-personne form').on('submit', function (event) {
747            var $form = $(this),
748                form_action = $form.prop('action'),
749                form_data = $form.serializeArray(),
750                $results = $search_modal.find('#udemwww-search-personne-results');
751
752            event.preventDefault();
753            pushSearchInputValueToDatalayer(this);
754
755            $results.html('<div>' + ll('Recherche en cours') + '</div>');
756
757            $.ajax({ url: form_action, data: form_data }).done(function (content, status, jqXhr) {
758                if (status === 'success' && typeof content === 'object' && content.data && typeof content.data.length !== 'undefined') {
759                    /* eslint-disable no-template-curly-in-string */
760                    if (content.data.length === 0) {
761                        // TODO localize
762                        $results.html('<div>' + ll('Aucun résultat') + '</div>');
763                    } else if (content.data.length === 1) {
764                        $results.html('<div>' + ll('Résultats : 1 personne trouvée') + '</div>');
765                    } else if (content.data.length === 100 || content.data.length === 200) {
766                        $results.html('<div>' + ll(max_reached_msg, { n: content.data.length }) + '</div>');
767                    } else {
768                        $results.html('<div>' + ll('Résultats : ${n} personnes trouvées', { n: content.data.length }) + '</div>');
769                    }
770                    /* eslint-enable no-template-curly-in-string */
771
772                    $(content.data).each(function (i, personne) {
773                        var $row = $('<div class="udemwww-search-personne-result"></div>'),
774                            $subrows;
775
776                        $('<span class="nom"></span>').text(personne.nomComplet || '').appendTo($row);
777
778                        if (personne.courriel) {
779                            $('<a class="email"></a>').text(personne.courriel).attr('href', 'mailto:' + personne.courriel).appendTo($row);
780                        }
781
782                        if (personne.fonction) {
783                            $('<span class="fonction"></span>').text(personne.fonction).appendTo($row);
784                        }
785
786                        if (personne.unite) {
787                            $('<span class="unite"></span>').text(personne.unite).appendTo($row);
788                        }
789
790                        if (personne.telephone) {
791                            $('<a class="tel"></a>').text(personne.telephone).attr('href', 'tel:' + personne.telephone.replace(/[^0-9#,]/g, '')).appendTo($row);
792                        }
793
794                        if (personne.pavillon || personne.local) {
795                            $('<span class="local"></span>').text(
796                                personne.pavillon
797                                    ? personne.pavillon + (personne.local ? ', local ' + personne.local : '')
798                                    : personne.local
799                            ).appendTo($row);
800                        }
801
802                        if (personne.fonctions && personne.fonctions.length) {
803                            $subrows = $('<div class="fonctions"></div>').appendTo($row);
804
805                            $(personne.fonctions).each(function (j, fonc) {
806                                var $row = $('<div class="fonc"></div>').appendTo($subrows);
807
808                                if (fonc.fonction) {
809                                    $('<span class="fonction"></span>').text(fonc.fonction).appendTo($row);
810                                }
811
812                                if (fonc.courriel) {
813                                    $('<a class="email"></a>').text(fonc.courriel).attr('href', 'mailto:' + fonc.courriel).appendTo($row);
814                                }
815
816                                if (fonc.unite) {
817                                    $('<span class="unite"></span>').text(fonc.unite).appendTo($row);
818                                }
819
820                                if (fonc.telephone) {
821                                    $('<a class="tel"></a>').text(fonc.telephone).attr('href', 'tel:' + fonc.telephone.replace(/[^0-9#,]/g, '')).appendTo($row);
822                                }
823
824                                if (fonc.pavillon || fonc.local) {
825                                    $('<span class="local"></span>').text(
826                                        fonc.pavillon
827                                            ? fonc.pavillon + (fonc.local ? ', local ' + fonc.local : '')
828                                            : fonc.local
829                                    ).appendTo($row);
830                                }
831                            });
832                        }
833
834                        $results.append($row);
835                    });
836                } else {
837                    $results.html('<div>' + ll('Une erreur est survenue durant la recherche') + '</div>');
838                }
839            }).fail(function (jqXhr, status, errorThrown) {
840                // The service does not provide a way to disciminate "not found" and error!
841                $results.html('<div>' + ll('Aucun résultat') + '</div>');
842            });
843        });
844    }
845
846    $(window).on('resize', udem_www.position_main_toolbar);
847    udem_www.position_main_toolbar();
848
849    // Send searched terms to GA
850    function pushSearchInputValueToDatalayer(formElement) {
851        // Get only input elements with type="search" within div with class "search-tabs"
852        const searchInputs = formElement.querySelectorAll('.search-tabs input[type="search"]');
853
854        // Add values to dataLayer
855        searchInputs.forEach(input => {
856            window.dataLayer.push({ 'event': 'recherche', 'query': input.value });
857        });
858    }
859
860    function addSearchListener() {
861
862        const searchTabsIds = ['udemwww-search-programme', 'udemwww-search-sofia'];
863        searchTabsIds.forEach((tabId) => {
864            // Attach event listener to submit event of forms within div with class "search-tabs"
865            const searchTabsForms = document.querySelectorAll('#' + tabId + ' form');
866            searchTabsForms.forEach(form => {
867                if (form.getAttribute("listener") !== "true") {
868                    form.setAttribute("listener", "true");
869                    form.addEventListener('submit', function (event) {
870                        event.preventDefault();
871                        pushSearchInputValueToDatalayer(this);
872                        this.submit()
873                    });
874                }
875            });
876        });
877    }
878
879    function search_show(callback) {
880        if ($('#udem-menu-tiroir').hasClass('udem-menu-tiroir-ouvert')) {
881            udem_www.menu_tiroir.close();
882        }
883
884        search_load_content(function () {
885            search_init_tabs();
886            gcse_init();
887
888            search_personne_form_init();
889
890            if (typeof callback !== 'undefined') {
891                callback();
892            }
893
894            addSearchListener();
895
896            // Update the location hash
897            window.location.hash = $search_modal.find('.search-tabbar a.active').attr('href');
898        });
899
900        setTimeout(search_init_close, 500); // must be defered, otherwize will be trigered immediatly (click is not finished yet?!)
901
902        $body.addClass('udem-show-search-modal');
903        udem_www.position_main_toolbar();
904        // console.log('TODO when search modal opened (but content not loaded yet!): ', this, arguments);
905    }
906
907    $search_trigger.on('click', function (event) {
908        event.preventDefault();
909        if (udem_www.search_is_shown()) {
910            search_hide();
911        } else {
912            search_show();
913        }
914        udem_www.position_main_toolbar();
915    });
916
917    function search_handle_hashchange() {
918        var regex = /(#udemwww-search-sites|#udemwww-search-programme|#udemwww-search-personne|#udemwww-search-sofia)(&.*)?$/,
919            hash = window.location.href.match(regex);
920        if (hash && hash[1]) {
921            if (!udem_www.search_is_shown()) {
922                search_show(function () {
923                    var $search_tabbar_items = $search_modal.find('.search-tabbar a'),
924                        $search_tabs_items = $search_modal.find('.search-tabs > *'),
925                        $tab;
926
927                    $search_tabbar_items.removeClass('active');
928                    $search_tabs_items.hide();
929
930                    $tab = $search_tabs_items.filter(hash[1]);
931                    if ($tab.length) {
932                        $search_tabbar_items.filter('[href="' + hash[1] + '"]').addClass('active');
933                        $tab.show();
934                        $tab.find('form input[type=search], form input[type=text]').first().trigger('focus');
935                    }
936                });
937            }
938        }
939    }
940
941    // on doc ready
942    setTimeout(search_handle_hashchange, 300);
943
944    // on navigation
945    $(window).on('hashchange', search_handle_hashchange);
946
947    // on anchor link click
948    $([
949        'a[href="#udemwww-search-sites"]',
950        'a[href="#udemwww-search-programme"]',
951        'a[href="#udemwww-search-personne"]',
952        'a[href="#udemwww-search-sofia"]'
953    ].join(',')).on('click', function (event) {
954        search_handle_hashchange();
955    });
956
957    // Needed to initialize the dedicated style element
958    udem_www.menu_tiroir.close();
959
960    $('#udem-menu-tiroir-trigger').on('click', function (event) {
961        var expand = !$(this).hasClass('active');
962
963        event.preventDefault();
964
965        if (expand) {
966            if (udem_www.search_is_shown()) {
967                search_hide();
968            }
969            udem_www.menu_tiroir.open();
970        } else {
971            udem_www.menu_tiroir.close(true);
972        }
973    });
974
975    $(window).on('resize scroll scrollstop', udem_www.debounce(function () {
976        var a = $('body').hasClass('udemwww-scrolled') && $('#udem-menu-tiroir').hasClass('udem-menu-tiroir-ouvert'),
977            b = $(window).width() < 1200;
978        if (a || b) {
979            udem_www.menu_tiroir.close();
980        }
981    }, 17));
982});
983
984// Equalize some block heights
985jQuery(function ($) {
986    'use strict';
987
988    var equalize_heights_selector = [
989            '.CType-textpic.lyt-3 .csc-header',
990            '.carousel-outer[data-carousel-slides-to-show="3"] .slick-slide > div > div > .content-container:not(.CType-textpic.lyt-3, .CType-image)',
991            '.carousel-outer[data-carousel-slides-to-show="2"] .slick-slide > div > div > .content-container:not(.CType-textpic.lyt-3, .CType-image)',
992            '.gbl-udembootstrap_2cols > .row > * > *:not(.CType-textpic.lyt-3, .CType-image)',
993            '.gbl-udembootstrap_3cols > .row > * > *:not(.CType-textpic.lyt-3, .CType-image)',
994            '.gbl-udembootstrap_4cols > .row > * > *:not(.CType-textpic.lyt-3, .CType-image)',
995            '.bloc-synthese-trigger'
996        ].join(','),
997
998        // Float-precision pixel units can mess-up some layouts: use ceiled dimensions!
999        // jQuery outerHeight method round the height in IE!
1000        getCeiledHeight = function (element) {
1001            if (typeof element.getBoundingClientRect === 'function') {
1002                try {
1003                    var rect = element.getBoundingClientRect();
1004                    return Math.ceil(rect.bottom - rect.top);
1005                } catch (e) {
1006                // In IE<=11, calling getBoundingClientRect on an element outside of the DOM throws an unspecified error instead of returning a 0x0 DOMRect
1007                // pass
1008                }
1009            }
1010            if (typeof window.getComputedStyle === 'function') {
1011                return Math.ceil(parseFloat(window.getComputedStyle(element, null).height));
1012            }
1013            return Math.ceil($(element).outerHeight());
1014        },
1015
1016        getFlooredOffsetTop = function (element) {
1017            return Math.floor($(element).offset().top);
1018        };
1019
1020    // Returns the next group of blocs to be processed, or undefined.
1021    // A group is a subset of the blocs:
1022    //   having the same offset top,
1023    //   having more than one bloc,
1024    //   having more than 1 distinct height.
1025    function next_group(blocs) {
1026        var groups = udem_www.groupBy(blocs, function (bloc) {
1027            return getFlooredOffsetTop(bloc);
1028        });
1029        return udem_www.find(groups, function (group) {
1030            var heights, min_height, max_height,
1031                headers, headers_heights,
1032                headers_min_height = 0,
1033                headers_max_height = 0;
1034
1035            if (group.length < 2) {
1036                return undefined;
1037            }
1038            if (group.length === 2 && group[0] === group[1].parentNode) {
1039                return undefined;
1040            }
1041
1042            heights = $.map(group, getCeiledHeight);
1043            min_height = Math.min.apply(null, heights);
1044            max_height = Math.max.apply(null, heights);
1045
1046            headers = $(group).children('.csc-header');
1047            if (headers.length > 1) {
1048                headers_heights = $.map(headers, getCeiledHeight);
1049                headers_min_height = Math.min.apply(null, headers_heights);
1050                headers_max_height = Math.max.apply(null, headers_heights);
1051            }
1052
1053            if (min_height === max_height && headers_min_height === headers_max_height) {
1054                return undefined;
1055            }
1056
1057            return true;
1058        });
1059    }
1060
1061    function equalize_heights(group) {
1062        var height, headers, headers_height;
1063
1064        height = Math.max.apply(null, $.map(group, getCeiledHeight));
1065        $(group).css({ height: height + 'px' });
1066
1067        headers = $(group).children('.csc-header');
1068        headers_height = Math.max.apply(null, $.map(headers, getCeiledHeight));
1069        $(headers).css({ height: headers_height + 'px' });
1070    }
1071
1072    function save_or_restore_original_height(blocs) {
1073        var i, origin;
1074        for (i = 0; i < blocs.length; i++) {
1075            if (blocs[i].hasAttribute('data-original-height')) {
1076                origin = blocs[i].getAttribute('data-original-height');
1077                $(blocs[i]).css('height', origin);
1078            } else {
1079                origin = blocs[i].style.height;
1080                if (origin === '') {
1081                    origin = 'auto';
1082                }
1083                blocs[i].setAttribute('data-original-height', origin);
1084            }
1085        }
1086    }
1087
1088    function reflow() {
1089        var $blocs = $(equalize_heights_selector),
1090            blocs, group;
1091
1092        if ($blocs.length === 0) {
1093            return;
1094        }
1095
1096        blocs = $blocs.get();
1097
1098        save_or_restore_original_height(blocs);
1099        save_or_restore_original_height($blocs.children('.csc-header').get());
1100
1101        for (let i = 0; i < 50; i++) { // limit to 50 passes, just in case...
1102            group = next_group(blocs);
1103            if (group) {
1104                equalize_heights(group);
1105            } else {
1106                break;
1107            }
1108        }
1109    }
1110
1111    $(window).on('resize', udem_www.debounce(reflow, 300));
1112    setTimeout(reflow, 50);
1113});
1114
1115// Accordeons udemwww_accordion.
1116jQuery(function ($) {
1117    'use strict';
1118
1119    var udemwww_accordion_TRANSITION_DURATION = 350,
1120        udemwww_accordion_trigger_selector = '.csc-header',
1121        udem_accordion_body_selector = '.udemwww_accordion-body';
1122
1123    // Get hash escaping the special char |.
1124    // Should use escapeSelector in Jquery 3 instead when used.
1125    function udemwww_get_escaped_hash() {
1126        return window.location.hash.replace(/\|/g, '\\|');
1127    }
1128
1129    function udemwww_accordion_openhash() {
1130        var hash = udemwww_get_escaped_hash().replace(/^#/, '').replace(/&.*$/, ''),
1131            accortion_selector = '.udemwww_accordion-item',
1132            $item,
1133            $identifier,
1134            is_accordion;
1135
1136        if (!hash) {
1137            return;
1138        }
1139
1140        $item = $('#' + hash);
1141        $identifier = $item;
1142        is_accordion = $identifier.is(accortion_selector);
1143
1144        // Check if the hash passed is the named identifier instead of the auto generated one.
1145        if (!is_accordion) {
1146            $identifier = $item.parent();
1147            is_accordion = $identifier.is(accortion_selector);
1148        }
1149
1150        if (is_accordion) {
1151            udemwww_accordion_show($identifier);
1152        }
1153    }
1154
1155    function udemwww_accordion_show($item) {
1156        var $trigger = $item.children(udemwww_accordion_trigger_selector),
1157            $element = $item.children(udem_accordion_body_selector),
1158            $parent = $item.parent(),
1159            hash;
1160
1161        // Activate/show ancestor tab-items/accordion-items
1162        $parent.parents().each(function () {
1163            var $ancestor = $(this);
1164
1165            if ($ancestor.is(udem_accordion_body_selector)) {
1166                udemwww_accordion_show($ancestor.parent());
1167            }
1168        });
1169
1170        // Put last opened accordion item in location hash.
1171        hash = $item.attr('id');
1172
1173        // Check the accordion item have a named identifier and use this one instead of the auto generated one.
1174        if ($item.children().first().is('a')) {
1175            hash = $item.children().first().attr('id');
1176        }
1177        if (hash) {
1178            if (typeof window.history.pushState === 'function') {
1179                window.history.pushState('', '', '#' + hash);
1180            } else {
1181                window.location.hash = '#' + hash;
1182            }
1183        }
1184
1185        $element.slideDown(udemwww_accordion_TRANSITION_DURATION, function () {
1186            $element.addClass('collapse in');
1187            $trigger.removeClass('collapsed');
1188        });
1189        $trigger.removeClass('collapsed');
1190    }
1191
1192    function udemwww_accordion_hide($item) {
1193        var $trigger = $item.children(udemwww_accordion_trigger_selector),
1194            $element = $item.children(udem_accordion_body_selector);
1195
1196        $element.slideUp(udemwww_accordion_TRANSITION_DURATION, function () {
1197            $element.removeClass('collapse in');
1198            $trigger.addClass('collapsed');
1199        });
1200    }
1201
1202    function udemwww_accordion_toggle($item) {
1203        var $element = $item.children(udem_accordion_body_selector);
1204
1205        if ($element.hasClass('in')) {
1206            udemwww_accordion_hide($item);
1207        } else {
1208            udemwww_accordion_show($item);
1209        }
1210    }
1211
1212    $(window).on('hashchange', udemwww_accordion_openhash);
1213    setTimeout(udemwww_accordion_openhash, 160);
1214
1215    $('.udemwww_accordion-container').each(function () {
1216        var $parent = $(this);
1217
1218        $parent.children().each(function () {
1219            var $item = $(this),
1220                $trigger = $item.children(udemwww_accordion_trigger_selector),
1221                $element = $item.children(udem_accordion_body_selector);
1222
1223            if (!$element.hasClass('in')) {
1224                $trigger.addClass('collapsed');
1225            }
1226
1227            $trigger.on('click', function (event) {
1228                var $item = $(this).parent();
1229                event.preventDefault();
1230                udemwww_accordion_toggle($item);
1231            });
1232        });
1233    });
1234});
1235
1236// Onglets udembootstrap_tabs
1237jQuery(function ($) {
1238    'use strict';
1239
1240    $('.gbl-udembootstrap_tabs').each(function () {
1241        var $container = $(this),
1242            $tabs = $container.children(),
1243            $tabbar = $('<ul class="tabs-tabbar"></ul>');
1244
1245        $container.addClass('tabs-container');
1246
1247        $tabs.each(function (i) {
1248            var $tab = $(this),
1249                id = $tab.attr('id'),
1250                $header = $tab.find('.csc-header').first(),
1251                label = $header.text(),
1252                $tabbar_item,
1253                $accordion_trigger,
1254                activator;
1255
1256            $tab.addClass('tabs-panel');
1257            $header.addClass('tabs-original-header');
1258
1259            $tabbar_item = $('<li><a href="#' + id + '">' + label + '</a></li>');
1260            $accordion_trigger = $('<div class="tabs-accordion-trigger"><a href="#' + id + '">' + label + '</a></div>');
1261
1262            $tabbar.append($tabbar_item);
1263            $tab.before($accordion_trigger);
1264
1265            activator = (function () {
1266                return function (event) {
1267                    event.preventDefault();
1268                    var tabsactiveclass = 'tabs-active',
1269                        expandedclass = 'expanded';
1270                    $tabbar.children().not($tabbar_item).removeClass(tabsactiveclass);
1271                    $container.children('.tabs-accordion-trigger').not($accordion_trigger).removeClass(tabsactiveclass);
1272                    $tabs.not($tab).removeClass(expandedclass);
1273
1274                    if ($accordion_trigger.hasClass(tabsactiveclass)) {
1275                        $accordion_trigger.removeClass(tabsactiveclass);
1276                        $tabbar_item.removeClass(tabsactiveclass);
1277                        $tab.removeClass(expandedclass);
1278                    } else {
1279                        $accordion_trigger.addClass(tabsactiveclass);
1280                        $tabbar_item.addClass(tabsactiveclass);
1281                        $tab.addClass(expandedclass);
1282                        // If mobile, scroll to accordion, else, scroll to tab.
1283                        if ($(this).is($accordion_trigger)) {
1284                            udem_www.scrollToElement($accordion_trigger, $('#udemwww-main-toolbar').height());
1285                        } else {
1286                            udem_www.scrollToElement($tab, $('#udemwww-main-toolbar').height());
1287                        }
1288                    }
1289                };
1290            }());
1291
1292            $accordion_trigger.on('click', activator);
1293            $tabbar_item.on('click', activator);
1294        });
1295
1296        $container.prepend($tabbar);
1297    });
1298});
1299
1300// "udemInViewport" jQuery special event.
1301// Adapted from https://github.com/protonet/jquery.inview/blob/master/jquery.inview.js (published under "DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE")
1302(function ($) {
1303    'use strict';
1304
1305    var inviewObjects = [],
1306        viewportSize = null, // cache of viewport size
1307        viewportOffset = null, // cache of viewport scroll amount
1308        d = document,
1309        w = window,
1310        docElement = d.documentElement,
1311        timer,
1312        getViewportOffset,
1313        checkinview_latency = 32; // latency in milliseconds. The lowest, the fastest, but the more stressfull for the layiout engine. Tests show that "32" adds 2% JS performance penalty compared to "120".
1314
1315    // Returns cross-browser equivalent of { top: window.scrollY, left: window.scrollX }
1316    getViewportOffset = (function () {
1317        if (typeof window.pageXOffset === 'undefined') {
1318            return function () {
1319                return {
1320                    top: document.documentElement.scrollTop,
1321                    left: document.documentElement.scrollLeft
1322                };
1323            };
1324        } else {
1325            return function () {
1326                return {
1327                    top: window.pageYOffset,
1328                    left: window.pageXOffset
1329                };
1330            };
1331        }
1332    }());
1333
1334    // TODO Profiling shows that this function really need more optimizations!
1335    function checkInView() {
1336        var i,
1337            $elements,
1338            $element,
1339            elementHeight,
1340            elementWidth,
1341            elementOffset,
1342            inView,
1343            viewportHeight,
1344            viewportWidth,
1345            viewportOffsetTop,
1346            viewportOffsetLeft;
1347
1348        if (!inviewObjects.length) {
1349            return;
1350        }
1351
1352        // Use or refresh viewportSize cache
1353        if (viewportSize === null) {
1354            viewportSize = {
1355                height: window.document.documentElement.clientHeight,
1356                width: window.document.documentElement.clientWidth
1357            };
1358        }
1359        // Create copies of values asap, to try to avoid race conditions with the events that invalidate viewportSize cache (TODO should have some kind of lock?)
1360        viewportHeight = viewportSize.height;
1361        viewportWidth = viewportSize.width;
1362
1363        // Use or refresh viewportOffset cache
1364        if (viewportOffset === null) {
1365            viewportOffset = getViewportOffset();
1366        }
1367        // Create copies of values asap, to try to avoid race conditions with the events that invalidate viewportOffset cache (TODO should have some kind of lock?)
1368        viewportOffsetTop = viewportOffset.top;
1369        viewportOffsetLeft = viewportOffset.left;
1370
1371        $elements = $.map(inviewObjects, function (inviewObject) {
1372            var selector = inviewObject.data.selector,
1373                $element = inviewObject.$element;
1374            return selector ? $element.find(selector) : $element;
1375        });
1376
1377        for (i = 0; i < $elements.length; i++) {
1378            // Ignore elements that are not in the DOM tree -- TODO optimize!
1379            if (!$.contains(docElement, $elements[i][0])) {
1380                continue;
1381            }
1382
1383            $element = $($elements[i]);
1384            elementHeight = $element[0].offsetHeight;
1385            elementWidth = $element[0].offsetWidth;
1386            elementOffset = $element.offset();
1387            inView = $element.data('udemInViewport_isInView');
1388
1389            if (elementOffset.top + elementHeight > viewportOffsetTop &&
1390                elementOffset.top < viewportOffsetTop + viewportHe
1390ight &&
1391                elementOffset.left + elementWidth > viewportOffsetLeft &&
1392                elementOffset.left < viewportOffsetLeft + viewportWidth) {
1393                if (!inView) {
1394                    $element.data('udemInViewport_isInView', true).trigger('udemInViewport', [true, $element.data('udemInViewport_wasInView')]);
1395                    $element.data('udemInViewport_wasInView', true);
1396                }
1397            } else if (inView) {
1398                $element.data('udemInViewport_isInView', false).trigger('udemInViewport', [false]);
1399            }
1400        }
1401    }
1402
1403    $.event.special.udemInViewport = {
1404        // @see http://learn.jquery.com/events/event-extensions/#add-function-handleobj
1405        add: function (handleObj) {
1406            inviewObjects.push({ data: handleObj, $element: $(this), element: this });
1407            if (!timer && inviewObjects.length) {
1408                // TODO this timer consumes a lot of cycles! Any way to avoid that?
1409                timer = setInterval(checkInView, checkinview_latency);
1410            }
1411        },
1412
1413        // @see http://learn.jquery.com/events/event-extensions/#remove-function-handleobj
1414        remove: function (handleObj) {
1415            for (let i = 0; i < inviewObjects.length; i++) {
1416                let inviewObject = inviewObjects[i];
1417                if (inviewObject.element === this && inviewObject.data.guid === handleObj.guid) {
1418                    inviewObjects.splice(i, 1);
1419                    break;
1420                }
1421            }
1422
1423            // Clear interval when we no longer have any elements listening
1424            if (!inviewObjects.length) {
1425                clearInterval(timer);
1426                timer = null;
1427            }
1428        }
1429    };
1430
1431    $(function () {
1432        // on document ready
1433        viewportSize = {
1434            height: window.document.documentElement.clientHeight,
1435            width: window.document.documentElement.clientWidth
1436        };
1437
1438        viewportOffset = getViewportOffset();
1439    });
1440
1441    // Reset viewportSize and viewportOffset when they may have changed
1442    $(w).on('scroll resize scrollstop', udem_www.debounce(function () {
1443        // TODO May be better to warmup with actual values (less job in checkInView function)...
1444        viewportSize = viewportOffset = null;
1445    }, 32));
1446
1447    // IE < 9 scrolls to focused elements without firing the "scroll" event
1448    if (!docElement.addEventListener && docElement.attachEvent) {
1449        docElement.attachEvent('onfocusin', function () {
1450            viewportOffset = null;
1451        });
1452    }
1453}(jQuery));
1454
1455jQuery(function ($) {
1456    'use strict';
1457
1458    // Loads Alertes with XHR into #udemwww-alertes-placeholder
1459    var $alertes_placeholder = $('#udemwww-alertes-placeholder').first(),
1460        alertes_url = $alertes_placeholder.attr('data-alertes-url'),
1461        l10n_precedent = $('html').attr('lang') === 'en' ? 'Previous alert' : 'Alerte précédente',
1462        l10n_suivant = $('html').attr('lang') === 'en' ? 'Next alert' : 'Alerte suivante';
1463
1464    function escapeHtml(string) {
1465        return String(string).replace(/[&<>"]/g, function (s) {
1466            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[s];
1467        });
1468    }
1469
1470    function arrow(btnClass, iconClass, label) {
1471        return '<button type="button" class="' + escapeHtml(btnClass) +
1472            '" tabindex="0" data-role="none" role="button" aria-label="' +
1473            escapeHtml(label) + '"><span class="' + escapeHtml(iconClass) +
1474            '"></span><span class="sr-only">' + escapeHtml(label) +
1475            '</span></button>';
1476    }
1477
1478    if ($alertes_placeholder.length < 1) {
1479        return;
1480    }
1481
1482    /*
1483    // Mobile (default): #udemwww-alertes-placeholder is nextsibling of #udemwww-main-header
1484    // Desktop (>=1200): #udemwww-alertes-placeholder is previoussibling of #udemwww-main-header
1485    function init_fix_alertes_placeholder() {
1486        function fix_alertes_placeholder() {
1487            var body_width = $('body').width(),
1488                next_id = $alertes_placeholder.next().attr('id'),
1489                prev_id = $alertes_placeholder.prev().attr('id'),
1490                $main_header = $('#udemwww-main-header');
1491
1492            if ($main_header.length !== 1) {
1493                return;
1494            }
1495            if (body_width >= 1200 && next_id !== 'udemwww-main-header') {
1496                console.log('                       DESKTOP!');
1497                $alertes_placeholder.insertBefore($main_header);
1498            } else if (body_width < 1200 && prev_id !== 'udemwww-main-header') {
1499                console.log('                       MOBILE!');
1500                $alertes_placeholder.insertAfter($main_header);
1501            }
1502        }
1503
1504        fix_alertes_placeholder();
1505        $(window).on('resize', fix_alertes_placeholder);
1506    }
1507    */
1508
1509    $.get(alertes_url, function (content, status, xhr) {
1510        var $inner1, $inner2;
1511        if (status === 'success' && content) {
1512            $inner1 = $('<div id="udemwww-alertes-placeholder-inner1"></div>').appendTo($alertes_placeholder);
1513            $inner2 = $('<div id="udemwww-alertes-placeholder-inner2"></div>').appendTo($inner1);
1514            $inner2.html(content);
1515
1516            $inner2.css({ margin: '0 64px' });
1517
1518            $inner2.slick({
1519                adaptiveHeight: true,
1520                prevArrow: arrow('slick-prev', 'udem-icon-ico-chevron-left', l10n_precedent),
1521                nextArrow: arrow('slick-next', 'udem-icon-ico-chevron-right', l10n_suivant),
1522                respondTo: 'slider'
1523            });
1524
1525            $('body').addClass('udem-has-alertes');
1526
1527            // init_fix_alertes_placeholder();
1528        }
1529    });
1530});
1531
1532// Calendrier
1533jQuery(function ($) {
1534    'use strict';
1535
1536    function escapeHtml(string) {
1537        return String(string).replace(/[&<>"]/g, function (s) {
1538            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[s];
1539        });
1540    }
1541
1542    function arrow(btnClass, iconClass, label) {
1543        return '<button type="button" class="' + escapeHtml(btnClass) +
1544            '" tabindex="0" data-role="none" role="button" aria-label="' +
1545            escapeHtml(label) + '"><span class="' + escapeHtml(iconClass) +
1546            '"></span><span class="sr-only">' + escapeHtml(label) +
1547            '</span></button>';
1548    }
1549
1550    $('.udemwww-calendrier-list-view').each(function (i, calendrier) {
1551        var $calendrier = $(calendrier),
1552            l10n_precedent = 'Activités précédentes', // TODO localize
1553            l10n_suivant = 'Activités suivantes'; // TODO localize
1554
1555        // Remember that 'init' event MUST be registered before calling 'slick()'!
1556        // Same thing for the 'setPosition' if you want those event for first layout !
1557
1558        // Equalize heights! (works only if adaptiveHeight option is false, because of animation...)
1559        $calendrier.on('setPosition', function (event, slick) {
1560            try {
1561                var height = slick.$list.height();
1562                slick.$slides.height(height);
1563            } catch (e) {
1564                // pass
1565                // console.log('CAUGHT Exception: ', e);
1566            }
1567        });
1568
1569        $calendrier.slick({
1570            autoplay: false,
1571            adaptiveHeight: false,
1572
1573            arrows: true,
1574            prevArrow: arrow('slick-prev', 'udem-icon-ico-chevron-left', l10n_precedent),
1575            nextArrow: arrow('slick-next', 'udem-icon-ico-chevron-right', l10n_suivant),
1576
1577            dots: false,
1578            infinite: false,
1579            speed: 300,
1580            // centerMode     : true,
1581            // centerPadding  : '80%',
1582
1583            slidesToShow: 1,
1584            slidesToScroll: 1,
1585
1586            mobileFirst: true,
1587            respondTo: 'window',
1588            responsive: [
1589                {
1590                    breakpoint: 767,
1591                    settings: {
1592                        slidesToShow: 2,
1593                        slidesToScroll: 1
1594                    }
1595                },
1596                {
1597                    breakpoint: 1199,
1598                    settings: {
1599                        slidesToShow: 3,
1600                        slidesToScroll: 1
1601                    }
1602                }
1603            ]
1604        });
1605    });
1606});
1607
1608// Carousel
1609/*
1610    .gbl-udembootstrap_carousel                        $carousel
1611        > a (?)
1612        > div.carousel-outer[data-carousel-...="..."]  $wrapper
1613            > .carousel-inner                          $container
1614                > .udembootstrap_carousel_item (*)
1615*/
1616jQuery(function ($) {
1617    'use strict';
1618
1619    var $carousels = $('.gbl-udembootstrap_carousel');
1620
1621    function escapeHtml(string) {
1622        return String(string).replace(/[&<>"]/g, function (s) {
1623            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[s];
1624        });
1625    }
1626
1627    function dataAttrBool($element, name, default_value) {
1628        var value = $element.attr('data-carousel-' + name);
1629        if (typeof value === 'undefined') {
1630            return !!default_value;
1631        }
1632        if (value === 'false' || value === '0' || value === '' || !!value === false) {
1633            return false;
1634        }
1635        return true;
1636    }
1637
1638    function dataAttrInt($element, name, default_value) {
1639        var value = $element.attr('data-carousel-' + name);
1640        if (typeof value !== 'undefined' && value.match(/^-?[0-9]+$/)) {
1641            return parseInt(value, 10);
1642        }
1643        return default_value;
1644    }
1645
1646    function arrow(btnClass, iconClass, label) {
1647        return '<button type="button" class="' + escapeHtml(btnClass) +
1648            '" tabindex="0" data-role="none" role="button" aria-label="' +
1649            escapeHtml(label) + '"><span class="' + escapeHtml(iconClass) +
1650            '"></span><span class="sr-only">' + escapeHtml(label) +
1651            '</span></button>';
1652    }
1653
1654    function makeCarouselConfig($wrapper) {
1655        var config,
1656            l10n_precedent = $('html').attr('lang') === 'en' ? 'Previous' : 'Précédent',
1657            l10n_suivant = $('html').attr('lang') === 'en' ? 'Next' : 'Suivant',
1658
1659            slidesToShow = dataAttrInt($wrapper, 'slides-to-show', 1);
1660            // slidesToScroll = dataAttrInt($wrapper, 'slides-to-scroll', 1),
1661            // arrows = dataAttrBool($wrapper, 'arrows', false)
1662        ;
1663
1664        config = {
1665            autoplay: dataAttrBool($wrapper, 'autoplay', false),
1666            autoplaySpeed: dataAttrInt($wrapper, 'autoplay-speed', 3000),
1667            adaptiveHeight: dataAttrBool($wrapper, 'adaptive-height', false),
1668            arrows: dataAttrBool($wrapper, 'arrows-mobile', false),
1669            dots: dataAttrBool($wrapper, 'dots', false),
1670            infinite: dataAttrBool($wrapper, 'infinite', false),
1671            speed: dataAttrInt($wrapper, 'speed', 300),
1672            prevArrow: arrow('slick-prev', 'udem-icon-btn-precedent', l10n_precedent),
1673            nextArrow: arrow('slick-next', 'udem-icon-btn-suivant', l10n_suivant),
1674            slidesToShow: 1,
1675            slidesToScroll: 1,
1676            mobileFirst: true,
1677            respondTo: 'window',
1678            responsive: [
1679                {
1680                    breakpoint: 767,
1681                    settings: {
1682                        centerMode: slidesToShow > 1,
1683                        centerPadding: '10%',
1684                        variableWidth: true,
1685                        arrows: false
1686                    }
1687                },
1688                {
1689                    breakpoint: 768,
1690                    settings: {
1691                        centerMode: false,
1692                        variableWidth: false,
1693                        arrows: dataAttrBool($wrapper, 'arrows', false),
1694                        slidesToScroll: dataAttrInt($wrapper, 'slides-to-scroll', 1)
1695                    }
1696                },
1697                {
1698                    breakpoint: 991,
1699                    settings: {
1700                        centerMode: false,
1701                        variableWidth: false,
1702                        slidesToShow: slidesToShow,
1703                        arrows: dataAttrBool($wrapper, 'arrows', false),
1704                        slidesToScroll: dataAttrInt($wrapper, 'slides-to-scroll', 1)
1705                    }
1706                }
1707            ]
1708        };
1709
1710        return config;
1711    }
1712
1713    $carousels.each(function (index, element) {
1714        var $carousel = $(element),
1715            $wrapper = $carousel.children('.carousel-outer').first(),
1716            $container = $wrapper.children('.carousel-inner'),
1717            config = makeCarouselConfig($wrapper),
1718            initial_slide = dataAttrInt($wrapper, 'initial-slide', 1) - 1,
1719            remember_and_increment = dataAttrBool($wrapper, 'remember-and-increment', false);
1720
1721        // Do not initialize the carousel if it has less than 2 items!
1722        if ($container.children().length < 2) {
1723            return;
1724        }
1725
1726        // Remember that 'init' event MUST be registered before calling 'slick()'!
1727        $container.on('init', function (ev, slick) {
1728            slick.$slides.children().children().css({ 'display': 'block' }); // CTTYPO3-1624
1729        });
1730
1731        $container.slick(config);
1732
1733        $container.slick('slickGoTo', initial_slide, true);
1734
1735        // Remember last current slide, and start at the next one on next visit
1736        if (remember_and_increment && typeof localforage !== 'undefined') {
1737            let remember_ns = 'udembootstrap_carousel_remember_and_increment',
1738                remember_store = localforage.createInstance({ name: remember_ns }),
1739                remember_key = $carousel.attr('id') || window.location.href + '#' + index,
1740                remember_getter = (function (store, key) {
1741                    return function (callback) {
1742                        store.getItem(key, callback);
1743                    };
1744                }(remember_store, remember_key)),
1745                remember_setter = (function (store, key) {
1746                    return function (value, callback) {
1747                        store.setItem(key, value, callback);
1748                    };
1749                }(remember_store, remember_key));
1750
1751            $container.on('afterChange', function (event, slick, currentSlideIndex) {
1752                // Stores current slide index
1753                remember_setter(currentSlideIndex);
1754            });
1755
1756            // On after slide change, put the dots inside the galerie item if in a square (imageorient-205).
1757            $container.on('afterChange', function(event, slick, currentSlide) {
1758                var $currentSlide = $container.find("[data-slick-index='"+ currentSlide + "']"),
1759                    $dots = $container.find('.slick-dots'),
1760                    $contentContainer = $currentSlide.find('.udembootstrap_carousel_item > .content-container');
1761
1762                if ($dots.length) {
1763                    if ($contentContainer.hasClass('imageorient-205')) {
1764                        $contentContainer.find('.csc-textpic-text-outer').append($dots);
1765                    } else if ($dots.parent() !== $container) {
1766
1767                        $container.append($dots);
1768                    }
1769                }
1770            });
1771
1772            remember_getter(function (err, remembered_slide_index) {
1773                if (typeof remembered_slide_index === 'number') {
1774                    // Calculating "next index" is quite difficult (because of all pagination options)!
1775                    // So we go to the stored slide index, and then call slickNext()...
1776                    $container.slick('slickGoTo', remembered_slide_index, true);
1777                    $container.slick('changeSlide', { data: { message: 'next' } }, true); // don't animate!
1778                } else {
1779                    remember_setter(initial_slide);
1780                }
1781            });
1782        }
1783    });
1784});
1785
1786// Titre avec lien => Tout le contenu est actif !
1787// Mais il ne faut pas empêcher les autres liens de fonctionner...
1788jQuery(function ($) {
1789    'use strict';
1790
1791    $('.content-container .csc-header a').each(function (i, header_link) {
1792        if (header_link.href) {
1793            var $content = $content = $(header_link).parents('.content-container').first(),
1794                makeClickHandler = function (header_link) {
1795                    return function (event) {
1796                        // Notice: we do not prevent default!
1797                        if (!$(event.target).is('a[href],button,input')) {
1798                            // Too bad we can not safely make a synthetic click event on the header_link...
1799                            if (header_link.target) {
1800                                window.open(header_link.href, header_link.target);
1801                            } else {
1802                                window.location.href = header_link.href;
1803                            }
1804                        }
1805                    };
1806                };
1807
1808            if ($content.length === 1) {
1809                $content.css({ cursor: 'pointer' });
1810                $content.on('click', makeClickHandler(header_link));
1811            }
1812        }
1813    });
1814});
1815
1816// Typographic sugars...
1817jQuery(function ($) {
1818    'use strict';
1819    $('h1 *,h2 *,h3 *,h4 *,h5 *, h6 *,h1,h2,h3,h4,h5,h6').each(function (i, h) {
1820        var htm = $(h).html(),
1821            rtest = /(^|\s|&nbsp;)([0-9]+)(st|nd|rd|th|er|re|de|d|e)(\s|&nbsp;|$)/,
1822            rrepl = /^([0-9]+)(st|nd|rd|th|er|re|de|d|e)$/g,
1823            chunks, n;
1824
1825        if (htm.match(rtest)) {
1826            chunks = htm.split(/(\s|&nbsp;)/g);
1827            for (n = 0; n < chunks.length; n++) {
1828                chunks[n] = chunks[n].replace(rrepl, '$1<sup>$2</sup>');
1829            }
1830            $(h).html(chunks.join(''));
1831        }
1832    });
1833});
1834
1835// Répertoire : expand/collapse group items
1836jQuery(function ($) {
1837    'use strict';
1838    $('.udemwww-repertoire-group').each(function () {
1839        var $group = $(this),
1840            $expand_btns = $group.find('.udemwww-repertoire-more-btn');
1841        if ($expand_btns.length < 1) {
1842            // No "more" button? Do not hide items!...
1843            return;
1844        }
1845        $group.addClass('udemwww-repertoire-hide-items');
1846        $expand_btns.on('click', function (event) {
1847            event.preventDefault();
1848            $group.toggleClass('udemwww-repertoire-hide-items');
1849        });
1850    });
1851});
1852
1853// TODO This looks like dead/unfinished code!
1854jQuery(function ($) {
1855    'use strict';
1856    var selector = [
1857        '.col-md-8:nth-child(odd) > .CType-textpic.lyt-3:nth-child(odd) .csc-header',
1858        '.col-md-6:nth-child(odd) > .CType-textpic.lyt-3:nth-child(odd) .csc-header',
1859        '.col-md-4:nth-child(odd) > .CType-textpic.lyt-3:nth-child(odd) .csc-header',
1860        '.col-md-8:nth-child(even) > .CType-textpic.lyt-3:nth-child(even) .csc-header',
1861        '.col-md-6:nth-child(even) > .CType-textpic.lyt-3:nth-child(even) .csc-header',
1862        '.col-md-4:nth-child(even) > .CType-textpic.lyt-3:nth-child(even) .csc-header'
1863    ].join(',');
1864
1865    $(selector).each(function () {
1866        var $block_portrait_header = $(this),
1867            $container = $block_portrait_header.parents('.content-container[data-udemwww-bgcoloropacity]').first();
1868
1869        if ($container.attr('data-udemwww-bgcoloropacity')) {
1870            // $block_portrait_header.css({  });
1871
1872            // var bgc = $block_portrait_header.css('backgroundColor');
1873            // TODO
1874            // bgc = bgc.replace(/^rgb\(/, 'rgba(').replace(/\)/, ', ' + ($container.attr('data-udemwww-bgcoloropacity') / 100) + ')')
1875            // $block_portrait_header.css({'backgroundColor': bgc});
1876
1877            // TODO if bgc luminance < 40%, color: '#000'
1878        }
1879    });
1880});
1881
1882jQuery(function ($) {
1883    'use strict';
1884
1885    $('.content-container[data-udemwww-bgcoloropacity]').each(function () {
1886        var $content = $(this),
1887            bgcoloropacity = $content.attr('data-udemwww-bgcoloropacity'),
1888            bgcolor = $content.css('backgroundColor'),
1889            m;
1890
1891        bgcoloropacity = parseInt(bgcoloropacity, 10) / 100;
1892
1893        if (bgcolor === 'transparent') {
1894            bgcolor = 'rgba(0,0,0,0)';
1895        }
1896
1897        m = bgcolor.match(/^(rgb|hsl)\s*\(\s*([0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*)\)/);
1898        if (m) {
1899            $content.attr('data-udemwww-original-bgcolor', bgcolor);
1900            $content.css({ 'backgroundColor': m[1] + 'a(' + m[2] + ', ' + bgcoloropacity + ')' });
1901            return;
1902        }
1903
1904        m = bgcolor.match(/^(rgb|hsl)a\s*\(\s*([0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*),\s*[0-9]+\s*\)/);
1905        if (m) {
1906            $content.attr('data-udemwww-original-bgcolor', bgcolor);
1907            $content.css({ 'backgroundColor': m[1] + 'a(' + m[2] + ', ' + bgcoloropacity + ')' });
1908            return;
1909        }
1910
1911        m = bgcolor.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
1912        if (m) {
1913            // console.log('MATCH #hex: ', m);
1914            // console.log('TODO!');
1915        }
1916    });
1917});
1918
1919// Bloc synthèse
1920jQuery(function ($) {
1921    'use strict';
1922
1923    var $blocks = $('.CType-text.lyt-8,.CType-textpic.lyt-8');
1924
1925    function fix_bloc_position($bloc) {
1926        var $content = $bloc.children('.bloc-synthese-content'),
1927            $col = $bloc.parent(),
1928            $row = $col.parent();
1929
1930        if ($row.hasClass('row') && $col.css('position') === 'relative') {
1931            let fix_margin_left = $col.offset().left - $row.offset().left,
1932                fix_content_width = $row.outerWidth(true);
1933
1934            if ($content.css('position') === 'absolute') {
1935                $content.css({
1936                    marginLeft: -fix_margin_left + 'px',
1937                    width: fix_content_width + 'px'
1938                });
1939            } else {
1940                $content.css({
1941                    marginLeft: 0,
1942                    width: '100%'
1943                });
1944            }
1945        } else {
1946            // Not in ".row > .col-*"
1947            if ($content.css('position') === 'absolute') {
1948                $content.css({ width: $col.width() });
1949            }
1950        }
1951    }
1952
1953    $(window).on('resize', udem_www.debounce(function () {
1954        $blocks.filter('.udem-bloc-synthese-expanded').each(function () {
1955            fix_bloc_position($(this));
1956        });
1957    }, 300));
1958
1959    $blocks.each(function () {
1960        var $bloc = $(this),
1961            $header = $bloc.find('> .csc-header').first(),
1962            $trigger = $('<div class="bloc-synthese-trigger"></div>'),
1963            $content = $('<div class="udemtheme-blanc bloc-synthese-content"></div>'),
1964            $close = $('<button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button>');
1965
1966        $trigger.append($header.find('h1,h2,h3,h4,h5,h6').first().clone());
1967        if ($trigger.find('[class^="udem-icon-"]').length === 0) {
1968            $trigger.children('h1,h2,h3,h4,h5,h6').first().prepend('<span class="udem-icon-ico-plus hidden-md hidden-lg smaller-icon"></span>');
1969        }
1970
1971        $content.append($close);
1972        $content.append($bloc.children());
1973
1974        $content.prependTo($bloc);
1975        $trigger.prependTo($bloc);
1976
1977        $close.on('click', function (event) {
1978            $bloc.removeClass('udem-bloc-synthese-expanded');
1979        });
1980
1981        $trigger.css({ cursor: 'pointer' });
1982        $trigger.on('click', function (event) {
1983            var win_scrollY;
1984            event.preventDefault();
1985            $blocks.not($bloc).removeClass('udem-bloc-synthese-expanded');
1986            $bloc.toggleClass('udem-bloc-synthese-expanded');
1987            if ($bloc.hasClass('udem-bloc-synthese-expanded')) {
1988                fix_bloc_position($bloc);
1989
1990                // Scroll to content if not two third of it at least is not visible, or if top of the element is above viewport
1991                win_scrollY = typeof window.pageXOffset === 'undefined' ? document.documentElement.scrollTop : window.pageYOffset;
1992                if (
1993                    ($content.offset().top - win_scrollY - $(window).height()) > $content.height() * -0.66 ||
1994                    $content.offset().top < win_scrollY
1995                ) {
1996                    udem_www.scrollToElement($trigger.get(0), $('#udemwww-main-toolbar').height());
1997                }
1998            }
1999        });
2000    });
2001});
2002
2003jQuery(function ($) {
2004    'use strict';
2005
2006    // Returns the element background image size (async, returns 0 if o image or on error).
2007    // WARNING: Cached (in data-udemwww-bgimg-height attribute) for performance reasons
2008    function getBackgroundImageHeight($element, callback) {
2009        var cached, url, img;
2010
2011        cached = $element.attr('data-udemwww-bgimg-height');
2012        if (typeof cached !== 'undefined') {
2013            callback(parseFloat(cached));
2014            return;
2015        }
2016
2017        url = $element.css('background-image');
2018        if (!url) {
2019            callback(0);
2020            return;
2021        }
2022
2023        url = url.match(/^url\("?(.+?)"?\)$/);
2024        if (!url || url.length !== 2 || !url[1]) {
2025            callback(0);
2026            return;
2027        }
2028        url = url[1];
2029
2030        img = new Image();
2031        img.onload = function () {
2032            if (typeof this.height !== 'undefined') {
2033                $element.attr('data-udemwww-bgimg-height', this.height);
2034                callback(this.height);
2035            } else {
2036                callback(0);
2037            }
2038        };
2039        img.onerror = function () {
2040            callback(0);
2041        };
2042        img.src = url;
2043    }
2044
2045    function parallax_update() {
2046        $('.parallax').each(function () {
2047            var $element = $(this),
2048                parallax_scale = $element.attr('data-udemwww-parallax-scale'),
2049                viewport_height = $(window).height(),
2050                element_height = $element.height(),
2051                element_offset_top = $element.offset().top,
2052                viewport_scrollY = typeof window.pageYOffset === 'undefined'
2053                    ? (document.documentElement || document.body.parentNode || document.body).scrollTop
2054                    : window.pageYOffset,
2055                etv = element_offset_top - viewport_scrollY;
2056
2057            // Case where we can abort early: element is not in viewport!
2058            if (etv < -element_height || etv > viewport_height) {
2059                // console.log('PARALLAX ABORT: OUT OF VIEWPORT!', $element, etv, element_height, viewport_height);
2060                return;
2061            }
2062
2063            if (parallax_scale) {
2064                parallax_scale = parseFloat(parallax_scale);
2065                parallax_scale = isNaN(parallax_scale) ? 1.0 : parallax_scale / 100;
2066            }
2067            if (!parallax_scale) {
2068                parallax_scale = 1.0;
2069            }
2070
2071            getBackgroundImageHeight($element, function (bgimg_height) {
2072                var parallax_ratio;
2073
2074                if (!bgimg_height) {
2075                    // console.log('    PARALLAX ABORT: BGIMG_HEIGHT IS FALSY: ', $element, bgimg_height);
2076                    return;
2077                }
2078
2079                parallax_ratio = (etv + element_height) / (viewport_height + element_height);
2080
2081                if (parallax_scale < 0) {
2082                    parallax_ratio = (1 - parallax_ratio) / -parallax_scale;
2083                } else if (parallax_scale > 0) {
2084                    parallax_ratio = parallax_ratio / parallax_scale;
2085                }
2086
2087                $element.css({
2088                    'background-position': '50% ' + (parallax_ratio * 100) + '%'
2089                });
2090            });
2091        });
2092    }
2093
2094    $(window).on('scroll resize load', udem_www.debounce(parallax_update, 16));
2095});
2096
2097jQuery(function ($) {
2098    'use strict';
2099
2100    $('.udemwww-portraits').each(function () {
2101        var $container = $(this),
2102            initial_count = parseInt($container.attr('data-udemwww-portraits-initial-count'), 10) || 10,
2103            $columns = $container.children(),
2104            lang = $('html').attr('lang') === 'en' ? 'en' : 'fr',
2105            $bottom,
2106            $trigger;
2107
2108        if ($columns.children().length <= initial_count) {
2109            return;
2110        }
2111
2112        $trigger = $('<button></button>').append($('<span></span>').text(lang === 'en' ? 'Show more portraits' : 'Voir plus de portraits'));
2113        $bottom = $('<div></div>').addClass('udemwww-portraits__bottom').append($trigger).insertAfter($container);
2114
2115        function show_next_page() {
2116            var $not_visibles = $columns.children().not(':visible'),
2117                n_cols = $columns.length;
2118
2119            if ($not_visibles.length <= initial_count) {
2120                $not_visibles.fadeIn();
2121                $bottom.fadeOut();
2122                return;
2123            }
2124
2125            $columns.each(function (c, column) {
2126                $(column).children().each(function (p, elm) {
2127                    $(elm).data('order', p + (c / n_cols));
2128                });
2129            });
2130
2131            $not_visibles.sort(function (a, b) {
2132                var aw = $(a).data('order'),
2133                    bw = $(b).data('order');
2134                return aw - bw;
2135            });
2136
2137            $not_visibles = $not_visibles.slice(0, initial_count);
2138
2139            $not_visibles.fadeIn();
2140
2141            if ($columns.children().not(':visible').length === 0) {
2142                $bottom.fadeOut();
2143            }
2144        }
2145
2146        $columns.children().hide();
2147        show_next_page();
2148
2149        $trigger.on('click', show_next_page);
2150    });
2151});
2152
2153jQuery(function ($) {
2154    'use strict';
2155
2156    // Transforms links with the "embed-as-audio" class. (CTTYPO3-1467)
2157    $('a.embed-as-audio').each(function () {
2158        let $a = $(this),
2159            src = this.href,
2160            desc = $a.text(),
2161            $div = $('<div class="embed-as-audio" controls></div>'),
2162            $iframe,
2163            $audio;
2164
2165        if (src.match(/^https:\/\/soundcloud.com\//)) {
2166            $iframe = $('<iframe width="100%" height="166" scrolling="no" frameborder="no"></iframe>');
2167            $iframe.attr('src', 'https://w.soundcloud.com/player/?url=' + window.encodeURIComponent(src));
2168            $iframe.appendTo($div);
2169        } else {
2170            $audio = $('<audio controls></audio>').attr('src', src);
2171            $audio.appendTo($div);
2172        }
2173
2174        $a.before($div);
2175
2176        if (desc) {
2177            $('<div class="embeded-audio-description"></div>').text(desc).insertAfter($div);
2178        }
2179
2180        $a.remove();
2181    });
2182});
2183
2184jQuery(function ($) {
2185    'use strict';
2186
2187    // Transforms links with the "embed-as-video" class. (CTTYPO3-1462)
2188    // Some browsers (guess which ones!) do no support RegExp named groups...
2189    // Too bad. It could have been implemented in a more generic fashion.
2190    $('a.embed-as-video').each(function () {
2191        let a = $(this),
2192            src = this.href,
2193            desc = a.text(),
2194            div = $('<div class="embed-as-video embed-responsive embed-responsive-16by9"></div>'),
2195            /* eslint-disable no-template-curly-in-string */
2196            sites = [
2197                {
2198                    regexp: /^(?:https?:\/\/|\/\/)?(?:www\.|m\.)?(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/watch\?.+&v=))([\w-]+)/,
2199                    src: 'https://www.youtube.com/embed/${0}?rel=0&showinfo=0'
2200                },
2201                {
2202                    regexp: /^(?:https?:\/\/)?(?:www\.)?(?:vimeo\.com)\/([-a-zA-Z0-9_]+)/,
2203                    src: 'https://player.vimeo.com/video/${0}?portrait=0&byline=0'
2204                },
2205                {
2206                    regexp: /^(?:https?:\/\/)?(?:(?:www\.)?dailymotion\.(?:com|alice\.it)\/(?:(?:[^"]*?)?video|swf)|dai\.ly)\/([a-z0-9]+)/,
2207                    src: 'https://www.dailymotion.com/embed/video/${0}'
2208                },
2209                {
2210                    regexp: /^(?:https?:\/\/)?(?:www\.)?(?:archive\.org)\/(?:details|embed)\/([-a-zA-Z0-9_]+)/,
2211                    src: 'https://archive.org/embed/${0}'
2212                }
2213            ];
2214            /* eslint-enable no-template-curly-in-string */
2215
2216        sites.every(function (element) {
2217            let match = src.match(element.regexp);
2218            if (match) {
2219                /* eslint-disable-next-line no-template-curly-in-string */
2220                src = element.src.replace('${0}', match[1]);
2221            }
2222        });
2223
2224        div.append($('<iframe class="embed-responsive-item" frameborder="0" allowfullscreen="" width="640" height="360"></iframe>').attr('src', src));
2225        a.before(div);
2226
2227        if (desc) {
2228            $('<div class="embeded-video-description"></div>').text(desc).insertAfter(div);
2229        }
2230
2231        a.remove();
2232    });
2233});
2234
2235jQuery(function ($) {
2236    'use strict';
2237    // Adds the "apply-overflow-x-hidden" class to ".gbl-udemwww_strate" elements
2238    // that have at least one ".csc-text-text > blockquote" element.
2239    // @see comment around CSS class definition (in "udem_www_main.less")
2240    $('.csc-text-text > blockquote').each(function () {
2241        $(this).parents('.gbl-udemwww_strate').addClass('apply-overflow-x-hidden');
2242    });
2243});
2244
2245// Text & Image with mid overflow above, center image orientation.
2246jQuery(function ($) {
2247    'use strict';
2248
2249    function set_margintop() {
2250        var $blocks = $('.CType-textpic.imageorient-214');
2251        $blocks.each(function () {
2252            var $imgcontainer = $(this).find('.csc-textpic-midoverflowabove .csc-textpic-imagewrap'),
2253                $outerwrap = $(this).find('.midoverflowabove-outerwrap'),
2254                overflow = ($imgcontainer.outerHeight() / 2) +
2255                            parseFloat($outerwrap.css('padding-top')) +
2256                            parseFloat($outerwrap.css('border-top-width'));
2257            $(this).css('padding-top', overflow + 'px');
2258            $imgcontainer.css('margin-top', -overflow + 'px');
2259        });
2260    }
2261
2262    $(window).on('resize scroll scrollstop', set_margintop);
2263    set_margintop();
2264});
2265
2266// Handles click on the "Share by email" in News plugins
2267jQuery(function ($) {
2268    'use strict';
2269    $('.ltp-news_pi1 a.share_email').on('click', function (event) {
2270        var $this = $(this),
2271            subject = $this.attr('data-subject'),
2272            body = $this.attr('data-body');
2273        console.log('subject, body: ', subject, body);
2274        event.preventDefault();
2275        window.location.href = 'mailto:?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
2276    });
2277});
2278
2279// Animate on scroll (AOS)
2280jQuery(function ($) {
2281    'use strict';
2282
2283    if (typeof AOS !== 'object') {
2284        return;
2285    }
2286
2287    // TODO use udemInViewport instead of AOS observer
2288    AOS.init();
2289});
2290
2291// La fonte "Lora" a une grande queue sous les Q
2292jQuery(function ($) {
2293    'use strict';
2294
2295    $('span.lettrine').each((i, elm) => {
2296        if (elm.textContent === 'Q') {
2297            elm.style.paddingBottom = '.166666em';
2298        }
2299    });
2300});

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.