PageSourceSearch

https://asprise.com/res/style/21a/asprise.js

js asprise.com collected 2026-09-25 00:34:40 UTC 27,731 bytes, 647 lines download raw bytes

1'use strict';
2
3function isOnMobile() {
4    return window.matchMedia("(max-width: 767px)").matches; // Mobile media-breakpoint-down(sm) [0, 767px]
5}
6
7function isOnDesktop() {
8    return window.matchMedia("(min-width: 768px)").matches; // Desktop @include media-breakpoint-up(md) [768px, ∞)
9}
10
11/** @return {Boolean} whether jQuery is available */
12function isJQueryAvailable() {
13    return !(typeof jQuery === 'undefined');
14}
15
16/** throws error if jQuery isn't available. */
17function throwsErrorIfJQueryUnavailable() {
18    if(! isJQueryAvailable()) {
19        throw new Error("jQuery is required, but it isn't available.");
20    }
21}
22
23/**
24 * Calling to find country info, e.g, {country_code: 'SG', country_name: 'Singapore'}
25 * @param callbackFunc function with one param of type JSON object {country_code: 'SG', country_name: 'Singapore'}
26 * @return {*} null if needs to do AJAX or JSON object like {country_code: 'SG', country_name: 'Singapore'} if cached locally.
27 */
28function getClientCountryInfo(callbackFunc) {
29    var countryCode = asprise_util.getFromCookieOrLocalStorage('country_code', null);
30    var countryName = asprise_util.getFromCookieOrLocalStorage('country_name', null);
31    var info = countryCode ? {country_code: countryCode, country_name: countryName} : null;
32    if(callbackFunc) {
33        if(info) { // from cookie
34            callbackFunc(info);
35        } else { // initiate POST
36            var jqxhr = $.post( "/util/geoloc", function(data) {
37                try {
38                    info = asprise_util.isString(data) ? JSON.parse(data) : data;
39                } catch (parseErr) {
40                    asprise_util.log("Not valid JSON: " + parseErr, true);
41                }
42                if(info && info['country_code']) {
43                    asprise_util.setCookieAndLocalStorage('country_code', info['country_code']);
44                }
45                if(info && info['country_name']) {
46                    asprise_util.setCookieAndLocalStorage('country_name', info['country_name']);
47                }
48                if(info && info['country_code']) {
49                    callbackFunc(info);
50                }
51            }).fail(function() {
52                asprise_util.log(asprise_util.getJqXhrReadableErrorMesg.apply(null, arguments), true);
53            });
54        }
55    }
56    return info;
57}
58
59/** Gets the scroll position in vertical coordinate; compatible with all browsers. */
60function getScrollY() {
61    var scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
62    return scrollY;
63}
64
65function logToConsole(mesg, isError) {
66    if(window.console) {
67        var dateStr = new Date().toLocaleTimeString();
68        if(isError) {
69            if(console.error) {
70                console.error(dateStr + " " + mesg);
71            } else {
72                console.log(dateStr + " " + "ERROR: " + mesg);
73            }
74        } else {
75            if(console.info) {
76                console.info(dateStr + " " + mesg);
77            } else {
78                console.log(dateStr + "" + " INFO: " + mesg);
79            }
80        }
81    } else { // no console
82        if(isError) {
83            if(window.alert) {
84                alert("ERROR: " + mesg);
85            }
86        }
87    }
88}
89
90
91/**
92 * StickyNav: sticks on top when its original position is scrolled invisible; compatible with all browsers.
93 * @param {string} stickyNavWrapperSelector selects sticky nav wrappers (i.e., in-place div to prevent reflow when sticky nav 
93changes display)
94 * @constructor
95 */
96var StickyNav = function(stickyNavWrapperSelector) {
97    // Ref: https://codepen.io/Guilh/pen/JLKbn
98    // Ref: https://github.com/acch/scrollpos-styler/blob/master/scrollPosStyler.js
99    this.navWrappers = $(stickyNavWrapperSelector);
100
101    window.addEventListener('resize', function (e) {
102        this.onScrollChange(e);
103    }.bind(this), false);
104
105    // https://developer.mozilla.org/en-US/docs/Web/Events/scroll
106    this.last_known_scroll_position = 0;
107    window.addEventListener('scroll', function(e) {
108        var scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
109        var direction = scrollY == this.last_known_scroll_position ? undefined :
110            (scrollY > this.last_known_scroll_position ? 'down' : 'up');
111        this.last_known_scroll_position = scrollY;
112        if (direction) {
113            this.onScrollChange(e, direction);
114        }
115    }.bind(this), false);
116
117    this.onScrollChange();
118};
119
120/**
121 * To be called if there is any scroll change - either user scrolls or window resized.
122 * @param {UIEvent} event either 'resize' or 'scroll'
123 * @param {string|undefined} scroll direction 'up', 'down' or undefined
124 */
125StickyNav.prototype.onScrollChange = function (event, scrollDirection) {
126    var verticalScrollPos = $(window).scrollTop();
127    for(var i = 0; i < this.navWrappers.length; i++) {
128        var navWrapperElement = this.navWrappers.get(i);
129        var top = $(navWrapperElement).offset().top;
130        var bottom = top + navWrapperElement.clientHeight; // Assumption: border-box, i.e., height includes padding and border, but not margin.
131        if(verticalScrollPos > top) {
132            navWrapperElement.classList.add('sticky-nav-above-screen');
133        } else {
134            navWrapperElement.classList.remove('sticky-nav-above-screen');
135        }
136
137        if(bottom > verticalScrollPos + window.innerHeight) { // Assumption: no horizontal scrolls
138            navWrapperElement.classList.add('sticky-nav-below-screen');
139        } else {
140            navWrapperElement.classList.remove('sticky-nav-below-screen');
141        }
142
143        if(verticalScrollPos <= top && bottom <= verticalScrollPos + window.innerHeight) {
144            navWrapperElement.classList.add('sticky-nav-on-screen');
145        } else {
146            navWrapperElement.classList.remove('sticky-nav-on-screen');
147        }
148
149        if(scrollDirection) {
150            navWrapperElement.setAttribute('scroll-direction', scrollDirection);
151        } else {
152            navWrapperElement.setAttribute('scroll-direction', '');
153        }
154    }
155};
156
157/**
158 * MegaMenu: displays drop down content items with morphing transition effect.
159 * @param {string} navbarSelector selector to select the top level navigation bar containing branding, triggers and other elements, e.g., <nav>
160 * @param {string} mmTriggerContainerSelector the immediate parent of mega menu triggers, e.g., <ul>
161 * @param {string} mmHolderSelector div.mm-holder, the top level container for mega menu items
162 * @param {Boolean} fullWidth applicable for desktop only: true (default) to have mega menu to occupy full screen width instead of individual sizes.
163 * @param {Boolean} clickToExpandItem applicable for desktop only: false (default) to use hover as triggering event on the triggers true to use clicks instead
164 * @constructor
165 */
166var MegaMenu = function(navbarSelector, mmTriggerContainerSelector, mmHolderSelector, modeConfigured, fullWidth, clickToExpandItem) {
167    this.modeCurrent = null;
168
169    this.navbarSelector = navbarSelector;
170    this.triggerContainerSelector = mmTriggerContainerSelector;
171    this.holderSelector = mmHolderSelector;
172    this.fullWidth = fullWidth == null ? true : fullWidth;
173    this.clickToExpandItem = clickToExpandItem == null ? false : clickToExpandItem;
174
175    this.setMode(modeConfigured);
176};
177
178/** Mega items are displayed separated in their original container. */
179MegaMenu.ModeDesktop    = 'desktop';
180/** Mega items are displayed inline directly under the trigger. */
181MegaMenu.ModeMobile     = 'mobile';
182/** Automatically switches between desktop and mobile */
183MegaMenu.ModeAuto       = 'auto';
184
185/**
186 * Remove event listeners set up in previous mode (if available) and add event listeners to triggers and items for the given mode.
187 * Does nothing if the the mode is unchanged.
188 * @param {string} modeConfigured any of MegaMenu.ModeDesktop or MegaMenu.ModeMobile
189 */
190MegaMenu.prototype.setMode = function (modeConfigured) {
191    if(modeConfigured != MegaMenu.ModeMobile && modeConfigured != MegaMenu.ModeDesktop && modeConfigured != MegaMenu.ModeAuto) {
192        throw new Error("Unsupported mode: " + modeConfigured);
193    }
194
195    this.modeConfigured = modeConfigured;
196    var modeToBe = modeConfigured == MegaMenu.ModeAuto ? (isOnDesktop() ? MegaMenu.ModeDesktop : MegaMenu.ModeMobile) : modeConfigured;
197
198    if(modeToBe == this.modeCurrent) { // same mode, nothing to do.
199        return;
200    }
201    if(this.modeCurrent) {
202        logToConsole('Switching mode to: ' + modeToBe + " from " + this.modeCurrent);
203    }
204    if(isJQueryAvailable()) {
205        jQuery(this.navbarSelector).removeClass(MegaMenu.ModeMobile).removeClass(MegaMenu.ModeDesktop);
206        jQuery(this.navbarSelector).addClass(modeToBe);
207    }
208
209    // 1. Clean up
210    if(this.modeCurrent != null) {
211        this.collapse();
212        this.triggers.forEach(function (el) {
213            var menuItemSelector = el.getAttribute('data-menu-selector');
214            var menuItemToSelect = menuItemSelector ? document.querySelector(menuItemSelector) : null;
215            if(menuItemToSelect) {
216                if(this.modeCurrent == MegaMenu.ModeDesktop) {
217                    this.collapseDesktop();
218
219                    if (this.clickToExpandItem) {
220                        el.classList.remove('clickToTrigger');              // el.classList.add('clickToTrigger');
221                        el.removeEventListener('click', this, false);       // el.addEventListener('click', this, false);
222                    } else {
223                        el.removeEventListener('mouseenter', this, false);
224                        el.removeEventListener('mouseleave', this, false);
225                    }
226                } else if(this.modeCurrent == MegaMenu.ModeMobile) {
227                    $(el).find('.content.collapseDesktop.show').collapse('hide');
228                    el.classList.remove('clickToTrigger');                    // el.classList.add('clickToTrigger');
229                    for(var i = 0; i < el.children.length; i++) {
230                        if(el.children[i].tagName.toUpperCase() == 'A') {
231                            el.children[i].removeEventListener('click', this, false);
232                        }
233                    }
234                }
235            }
236        }.bind(this));
237
238        if(this.modeCurrent == MegaMenu.ModeDesktop && !this.clickToExpandItem) {
239            this.holder.removeEventListener('mouseenter', this, false);
240            this.holder.removeEventListener('mouseleave', this, false);
241        }
242        this.items.forEach(function (el) {
243            el.classList.remove('active');
244            el.style.opacity = 0;
245        }.bind(this));
246    }
247
248    // 2. Switch mode
249    this.modeCurrent = modeToBe; // switch mode now
250
251    // 3. Set and add
252    this.navbar = document.querySelector(this.navbarSelector);
253    this.triggerContainer = document.querySelector(this.triggerContainerSelector);
254    /** @var {Element[]} */
255    this.triggers = [].slice.call(this.triggerContainer.querySelectorAll('.mmi-trigger'));
256    this.selectedTrigger = undefined;
257
258    this.holder = document.querySelector(this.holderSelector);
259    this.arrow = this.holder.querySelector('.mm-arrow');
260    this.bg = this.holder.querySelector('.mm-bg');
261    this.bgBottom = this.holder.querySelector('.mm-bg-bottom');
262    /** @var {Element} Element div.mm-items, direct parent of div.mmi's */
263    this.container = this.holder.querySelector('.mm-items');
264    /** @var {Element[]} Array of div.mmi */
265    this.items = [].slice.call(this.container.querySelectorAll('.mmi'));
266
267    this.triggers.forEach(function (el) {
268        var menuItemSelector = el.getAttribute('data-menu-selector');
269        var menuItemToSelect = menuItemSelector ? document.querySelector(menuItemSelector) : null;
270        if(menuItemToSelect) {
271            el.classList.add('hasMegaMenu');
272            if(this.modeCurrent == MegaMenu.ModeDesktop) {
273                if (this.clickToExpandItem) {
274                    el.classList.add('clickToTrigger');
275                    el.addEventListener('click', this, false);
276                } else {
277                    el.addEventListener('mouseenter', this, false);
278                    el.addEventListener('mouseleave', this, false);
279                }
280            } else if(this.modeCurrent == MegaMenu.ModeMobile) {
281                el.classList.add('clickToTrigger');
282                // we can't listen to click on the whole <li> as content will be a descendant of <li>
283                for(var i = 0; i < el.children.length; i++) {
284                    if(el.children[i].tagName.toUpperCase() == 'A') {
285                       el.children[i].addEventListener('click', this, false);
286                    }
287                }
288                var contentContainer = el.querySelector('.content');
289                if(contentContainer && contentContainer.childElementCount == 0) { // we need to copy over
290                    throwsErrorIfJQueryUnavailable();
291                    contentContainer.classList.remove('show'); // depends on Bootstrap v4
292                    contentContainer.classList.add('collapse');
293
294                    var cloned = $(menuItemToSelect).clone().removeAttr('id');
295                    if(cloned.css('opacity') == 0) { // somehow, css style is set.
296                        cloned.css('opacity', '');
297                    }
298                    cloned.appendTo(contentContainer);
299                }
300            }
301        } else {
302            el.classList.remove('hasMegaMenu');
303        }
304    }.bind(this));
305
306    if(this.modeCurrent == MegaMenu.ModeDesktop && !this.clickToExpandItem) {
307        this.holder.addEventListener('mouseenter', this, false);
308        this.holder.addEventListener('mouseleave', this, false);
309    }
310    this.items.forEach(function (el) {
311        el.classList.remove('active');
312        el.style.opacity = 0;
313    }.bind(this));
314
315    if(this.modeConfigured == MegaMenu.ModeAuto) {
316        throwsErrorIfJQueryUnavailable();
317        window.addEventListener('resize', function () {
318            this.setMode(isOnDesktop() ? MegaMenu.ModeDesktop : MegaMenu.ModeMobile);
319        }.bind(this), false);
320    }
321
322    return this;
323};
324
325/**
326 *
327 * @param {MouseEvent} event
328 */
329MegaMenu.prototype.handleEvent = function(event) {
330    var elementCurrent = event.currentTarget; // the element to which the event handler has been attached
331    var handled = false;
332    if(this.modeCurrent == MegaMenu.ModeDesktop) { // Desktop
333        if (this.triggers.indexOf(elementCurrent) >= 0) { // is a trigger
334            var action = null;
335            if (this.clickToExpandItem) {
336                if (event.type == 'click') {
337                    action = elementCurrent.classList.contains('active') ? 'collapse' : 'expand';
338                }
339            } else { // hover
340                if (event.type == 'mouseenter') {
341                    action = 'expand';
342                } else if (event.type == 'mouseleave') {
343                    action = 'collapse';
344                }
345            }
346
347            if (action == 'expand') {
348                elementCurrent.classList.add('active');
349                // get mega menu id
350                var menuItemSelector = elementCurrent.getAttribute('data-menu-selector');
351                var menuItemToSelect = menuItemSelector ? document.querySelector(menuItemSelector) : null;
352                if (menuItemToSelect) {
353                    this.stopCollapseDesktop();
354                }
355                this.expandDesktop(elementCurrent, menuItemToSelect);
356                handled = true;
357            } else if (action == 'collapse') {
358                // elementCurrent.classList.remove('active');
359                this.startCollapseDesktopDesktop();
360                handled = true;
361            }
362        } else if (this.holder == elementCurrent || this.items.indexOf(elementCurrent) >= 0) { // is a mega menu item
363            if (!this.clickToExpandItem) {
364                if (event.type == 'mouseenter') {
365                    this.stopCollapseDesktop();
366                    handled = true;
367                } else if (event.type == 'mouseleave') {
368                    this.startCollapseDesktopDesktop();
369                    handled = true;
370                }
371            }
372        }
373    } else if(this.modeCurrent == MegaMenu.ModeMobile) { // Mobile
374        // Clicked on a trigger's first child
375        if(event.type == 'click' && this.triggers.indexOf(elementCurrent.parentNode) >= 0) { // e.g., event type: click; current target: A.nav-link
376            var triggeringElement = elementCurrent.parentNode; // LI
377            this.expandMobile(triggeringElement);
378            handled = true;
379        }
380    }
381
382    if(! handled) {
383        console.warn("Not handled: " + event.type + ': current target: ' + elementCurrent.tagName + '.' + elementCurrent.classList);
384    }
385};
386
387/**
388 * Applicable to Mobile only; Sets the given trigger to active and expand the content; closes the other active trigger if any.
389 * @param {Element} triggeringElement
390 */
391MegaMenu.prototype.expandMobile = function(triggeringElement) {
392    this.selectedTrigger = triggeringElement;
393    if(this.selectedTrigger == null) {
394        this.collapseMobile();
395        return;
396    }
397
398    var contentToToggle = triggeringElement.querySelector('.content');
399    if (contentToToggle == null) {
400        logToConsole('.content is not set', true);
401    } else {
402        var active = ! triggeringElement.classList.contains('active');
403        if(active) { // toggle active
404            triggeringElement.classList.add('active');
405        } else {
406            triggeringElement.classList.remove('active');
407        }
408
409        $(contentToToggle).collapse(active ? 'show' : 'hide');
410
411        if(active) { // collapse others
412            $(triggeringElement.parentNode).find('.content').each(function() { // collapse other content
413                if(this != contentToToggle) {
414                    $(this).collapse('hide');
415                }
416            });
417
418            $(triggeringElement.parentNode).children().each(function() { // de-active other triggers
419                if(this != triggeringElement) {
420                    this.classList.remove('active');
421                }
422            });
423        }
424    }
425};
426
427/** Applicable to Mobile only; removes 'active' class from all triggers and items */
428MegaMenu.prototype.collapseMobile = function() {
429    this.selectedTrigger = undefined;
430    this.triggers.forEach(function (el) {
431        el.classList.remove('active');
432        $(el).find('.content').each(function() { // collapse all content
433            $(this).collapse('hide');
434        });
435    });
436    if(this.triggerContainer.parentNode.classList.contains('show')) {
437        $(this.triggerContainer.parentNode).collapse('hide'); // $('.navbar-collapse').collapse('hide');
438    }
439};
440
441/** Collpase Desktop/Mobile depending on the current mode. */
442MegaMenu.prototype.collapse = function() {
443    if(this.modeCurrent == MegaMenu.ModeDesktop) {
444        this.collapseDesktop();
445    } else if(this.modeCurrent == MegaMenu.ModeMobile) {
446        this.collapseMobile();
447    }
448};
449
450/**
451 * Applicable to Desktop only; expands the given item.
452 * @param {Element} triggerToSelect the trigger
453 * @param {Element} itemToSelect the div.mmi to be expanded
454 */
455MegaMenu.prototype.expandDesktop = function (triggerToSelect, itemToSelect) {
456    this.selectedTrigger = triggerToSelect;
457    this.selectedItem = itemToSelect;
458    if(itemToSelect == null) {
459        this.startCollapseDesktopDesktop();
460        return;
461    }
462
463    if(! triggerToSelect) {
464        throw 'mm item to select is set, but trigger is not set.';
465    }
466
467    var itemContent = itemToSelect.querySelector('.mmi-content');
468    var itemTop = itemContent.querySelector('.mmi-top');
469    var itemBottom = itemContent.querySelector('.mmi-bottom');
470
471    var rectTrigger = triggerToSelect.getBoundingClientRect();
472    var rectTriggerContainer = document.querySelector(this.triggerContainerSelector).getBoundingClientRect();
473    var rectItemContent = itemContent.getBoundingClientRect();
474
475    this.triggers.forEach(function (el) {
476        return el.classList.remove('active');
477    });
478    triggerToSelect.classList.add('active');
479
480    this.items.forEach(function (el) {
481        el.classList.remove('active');
482        el.style.opacity = 0;
483    });
484    itemToSelect.classList.add('active');
485
486    this.bg.style.opacity = 1;
487    if(! this.fullWidth) {
488        this.bg.style.left = rectTrigger.left - (rectItemContent.width / 2 - rectTrigger.width / 2) + 'px';
489        this.bg.style.width = rectItemContent.width + 'px';
490    }
491    this.bg.style.height = rectItemContent.height + 'px';
492    this.bgBottom.style.top = itemTop.offsetHeight + 'px';
493
494    this.arrow.style.opacity = 1;
495    this.arrow.style.left = rectTrigger.left + rectTrigger.width / 2 - 10 + 'px'; // CP
496
497    this.container.style.opacity = 1;
498    //this.container.style.top = 0;
499    if(! this.fullWidth) {
500        this.container.style.left = this.bg.style.left;
501        this.container.style.width = this.bg.style.width;
502    }
503    this.container.style.height = this.bg.style.height;
504
505    itemToSelect.style.opacity = 1;
506    this.holder.classList.add('active');
507
508    document.querySelector('body').classList.add('mega-menu-expanded');
509    document.querySelector(this.navbarSelector).classList.add('mega-menu-expanded');
510    // console.log("+ mega-menu-expanded");
511};
512
513/** Applicable to Desktop only; removes 'active' class from all triggers and items; remove 'expanded' from holder. */
514MegaMenu.prototype.collapseDesktop = function() {
515    this.selectedTrigger = undefined;
516    this.triggers.forEach(function (el) {
517        return el.classList.remove('active');
518    });
519
520    this.items.forEach(function (el) {
521        el.classList.remove('active');
522        el.style.opacity = 0;
523    });
524
525    this.bg.style.opacity = 0;
526    this.arrow.style.opacity = 0;
527    this.container.style.opacity = 0;
528    this.holder.classList.remove('active');
529
530    document.querySelector('body').classList.remove('mega-menu-expanded');
531    document.querySelector(this.navbarSelector).classList.remove('mega-menu-expanded');
532    // console.log("- mega-menu-expanded");
533};
534
535/** Applicable to Desktop only; triggers this.collapseDesktop after 50m */
536MegaMenu.prototype.startCollapseDesktopDesktop = function () {
537    if(asprise_util.getUrlGetRequestParam('megamenu_debug')) {
538        console.log("MegaMenu collapse disabled.");
539        return;
540    }
541    this.collapseDesktopTimeoutId = setTimeout(function () {
542        var result = this.collapseDesktop();
543        this.collapseDesktopTimeoutId = undefined;
544        return result;
545    }.bind(this), // bind 'this' to this
546        50 // in milliseconds
547    );
548};
549
550/** Applicable to Desktop only; cancels collapse timeout if it exists */
551MegaMenu.prototype.stopCollapseDesktop = function () {
552    if(this.collapseDesktopTimeoutId == null) {
553        return;
554    }
555    clearTimeout(this.collapseDesktopTimeoutId);
556    this.collapseDesktopTimeoutId = undefined;
557};
558
559/**
560 * Creates and enables the mega menu; listens to collapse event of the navbar to collapse the mega menu.
561 * @param {string} navbarSelector default to '#navbar-site'
562 * @param {string} mmTriggerContainerSelector default to 'ul.navbar-nav'
563 * @param {string} mmHolderSelector default to 'div.mm-holder'
564 * @param {string} modeConfigured any of MegaMenu.ModeDesktop or MegaMenu.ModeMobile
565 * @return {MegaMenu} the mega menu created.
566 */
567function enableMegaMenu(navbarSelector, mmTriggerContainerSelector, mmHolderSelector, modeConfigured) {
568    navbarSelector = navbarSelector || '#navbar-site';
569    var mega = new MegaMenu(navbarSelector, mmTriggerContainerSelector || 'ul.navbar-nav', mmHolderSelector || 'div.mm-holder', modeConfigured, true, false);
570    $(navbarSelector +  ' .navbar-collapse').on('hidden.bs.collapse', function (event) {
571        if (event.target.classList.contains('navbar-collapse')) { // skip event from nested collapse
572            if (event.type.indexOf('hid') >= 0) {
573                mega.collapse(); // Collapse mega menu if navbar-site is collapsed
574            }
575        }
576    });
577    return mega;
578}
579
580/**
581 * Creates a headroom and init it; does nothing and returns null if the selector results no element.
582 * @param {string} docQuerySelectorToElement used to query the element to be headroomed, default to ".headroom"
583 * @param {int} offset don't unpin for the number of pixels scrolled
584 * @return {Headroom}
585 */
586function enableHeadroom(docQuerySelectorToElement, offset) {
587    docQuerySelectorToElement = docQuerySelectorToElement || ".headroom";
588    if(! document.querySelector(docQuerySelectorToElement)) {
589        return null;
590    }
591    var hr = new Headroom(document.querySelector(docQuerySelectorToElement), {
592        offset : offset || 10, // don't unpin for the top 160 pixel scrolled
593        onUnpin: function () {
594            // mega.collapse();
595            // $('.navbar-collapse').collapse('hide');
596        }
597    });
598    hr.init(); // display nav when scrolling up
599    return hr;
600}
601
602/**
603 * Enables typing using typed.js; does nothing if the selector fails to select an element.
604 * @param {string} typedQuerySelector to be used in document.querySelector, defaults to '#typed'
605 * @param {int} waitTimeBeforeStartInMs, defaults to 3000
606 * @param {string[]} strings defaults to ['Scan Directly in Web Apps', ...]
607 */
608function enableTyping(typedQuerySelector, waitTimeBeforeStartInMs, strings) {
609    typedQuerySelector = typedQuerySelector || '#typed';
610    if(! document.querySelector(typedQuerySelector)) {
611        return null;
612    }
613    // typing effect
614    setTimeout(function() {
615        var typed = new Typed(typedQuerySelector, strings || {
616            strings: ["Scan Directly in Web Apps",
617                "Scan Directly in Dropbox",
618                "Scan Directly in Gmail",
619                "Scan Directly in Office 365",
620                "Scan Directly in QBO",
621                "Scan Directly in Salesforce",
622                "Scan Directly in SAP",
623                "Scan Directly in Web Apps"
624            ],
625            typeSpeed: 160,
626            backSpeed: 0,
627            backDelay: 500,
628            startDelay: 0, // use the outside setTimeout to avoid cursor appears at the end while not typing.
629            loop: false,
630            onComplete: function(self) {
631                self.cursor.classList.add('typed-complete');
632            }
633        });
634    }, (typeof waitTimeBeforeStartInMs === 'undefined') ? 3000 : waitTimeBeforeStartInMs); // wait for 3s then start
635}
636
637/**
638 * Enables sticky nav for the given nav wrapper selector; does nothing and returns null if the selector fails to select any element.
639 * @param {string} stickyNavWrapperSelector default to '.sticky-nav-wrapper'
640 */
641function enableStickyNav(stickyNavWrapperSelector) {
642    stickyNavWrapperSelector = stickyNavWrapperSelector || '.sticky-nav-wrapper';
643    if($(stickyNavWrapperSelector).length == 0) {
644        return null;
645    }
646    return new StickyNav(stickyNavWrapperSelector);
647}

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.