PageSourceSearch

https://www.pet911vet.com/plugins/smb/navigation/components/navigation/assets/js/navigation.js?v.1.1.40

js pet911vet.com collected 2026-09-28 13:13:13 UTC 21,833 bytes, 607 lines download raw bytes

1(($) => {
2'use strict';
3
4/** @description document.documentElement.clientWidth <= 992 */
5const megaMenuBreakpoint = window.matchMedia('(max-width: 992px)');
6//SMBWMGR-7625- hide mmenu before init on theme level
7try {
8    /** @type {HTMLStyleElement}*/
9    const style = document.createElement('STYLE');
10    document.head.append(style);
11    style.sheet.insertRule(`.mm-menu.mm-offcanvas { display: none; }`);
12} catch (e) {
13    console.error(e);
14}
15
16
17let firstMenuItemResizeObserve;
18const menuItemResizeObserver = new ResizeObserver((_entries, o) => {
19    // ignore the event when it fires upon adding items
20    if (firstMenuItemResizeObserve) {
21        firstMenuItemResizeObserve = false;
22        return;
23    }
24    // re-initialize navigations, no matter what item caused the event
25    schedulePositionNavs();
26});
27
28let positionNavsTimer = 0;
29
30function schedulePositionNavs() {
31    if (positionNavsTimer) return;
32    const myTimer = positionNavsTimer = requestAnimationFrame(() => {
33        if (myTimer !== positionNavsTimer) return;
34        positionNavs();
35        positionNavsTimer = 0;
36    });
37}
38
39function positionNavs() {
40    NavigationAMoreList.reposition();
41    // navigations
42    MegaMenu.reposition();
43}
44
45function addMMenuTitleAttributes() {
46    // add title attributes to mobile menu (mm-menu) no text links - SMBWMGR-3784
47    for (const el of document.querySelectorAll('nav.mm-menu a.mm-prev:not([title])')) {
48        const title = el.parentElement.querySelector('a.mm-title')?.textContent;
49        if (title) el.setAttribute('title', title);
50    }
51
52    for (const el of document.querySelectorAll('nav.mm-menu a.mm-next:not([title])')) {
53        /** @type {null|HTMLElement} */
54        const titleEl = el.parentElement.querySelector(':scope > a[data-title]');
55        const title = titleEl?.dataset.title;
56        if (title) el.setAttribute('title', title);
57    }
58
59    for (const el of document.querySelectorAll('nav.mm-menu a.mm-close:not([title])')) {
60        el.setAttribute('title', 'Close');
61    }
62}
63
64class NavigationAMoreList {
65    static Templates = new class {
66        #style1;
67        /** @type {DocumentFragment} */
68        get style1() {
69            return this.#style1();
70        };
71
72        #style2;
73        /** @type {DocumentFragment} */
74        get style2() {
75            return this.#style2();
76        }
77
78        #styleDefault;
79        /** @type {DocumentFragment} */
80        get styleDefault() {
81            return this.#styleDefault();
82        }
83
84        constructor() {
85            const lazyFragment = (templateString) => {
86                /** @type {DocumentFragment} */
87                let cache;
88                return () => {
89                    if (cache) return cache;
90                    /** @type {HTMLTemplateElement} */
91                    const templateEl = document.createElement('TEMPLATE');
92                    templateEl.innerHTML = templateString;
93                    cache = templateEl.content;
94                    return cache;
95                };
96            };
97
98            this.#style1 = lazyFragment('<li class="navigation__item navigation__item--default has-drop-down show-drop-down drop-down-style--1 more"> <a href="#" class="navigation__item--link navigation__item--expand_more">More<span class="navigation__item--expand" title="View more" aria-expanded="false"></span> </a><ul class="navigation__list navigation__list--default navigation__list--sub"></ul> </li>');
99            this.#style2 = lazyFragment('<li class="navigation__item navigation__item--default has-drop-down show-drop-down drop-down-style--2 more"> <a href="#" class="navigation__item--link  navigation__item--expand_more">More<span class="navigation__item--expand" title="View more" aria-expanded="false"></span> </a><ul class="navigation__list navigation__list--default navigation__list--sub"></ul> </li>');
100            this.#styleDefault = lazyFragment('<li class="navigation__item navigation__item--default has-drop-down more"> <a href="#" class="navigation__item--link navigation__item--expand_more">More<span class="navigation__item--expand" title="View more" aria-expanded="false"></span> </a><ul class="navigation__list navigation__list--default navigation__list--sub"></ul> </li>');
101        }
102    };
103
104    static unmask() {
105        const loadingMask = document.querySelectorAll('.navigation-a > .navigation__list.more_nav--loading');
106        for (const el of loadingMask) {
107            el.classList.remove('more_nav--loading');
108        }
109        return this;
110    }
111
112    /**
113     * @param  {HTMLElement} el
114     * @param {Partial<typeof NavigationAMoreList.Defaults>} [defaults]
115     * @return {NavigationAMoreList}
116     */
117    static create(el, defaults) {
118        const instance = new this(el, defaults);
119        this.active.add(instance);
120        return instance;
121    }
122
123    /**
124     * A Set of the Navigation Menus with More enabled, populated post ready
125     * @type {Set<NavigationAMoreList>}
126     */
127    static active = new Set();
128
129    static reposition() {
130        // Handle Nav A with More button repositioning
131        for (const navAMore of this.active) {
132            if (navAMore.arrange() === false) this.active.delete(navAMore);
133        }
134    }
135
136    /**
137     * @type {WeakRef<HTMLElement>}
138     */
139    ref;
140    /**
141     * @type {{el: HTMLElement; initalStyles: string; initalClasses: string; reset():void; hide():void;}}
142     */
143    more;
144
145    get el() {
146        return this.ref.deref();
147    }
148
149    /**
150     * @description The Cached settings of the initial direct children used for tracking and restoring positioning of
151     *  the Menu items in Navigation A
152     * @type {Array<NavigationAMoreMenuItem>}
153     */
154    initalState;
155
156    /**
157     *
158     * @param  {HTMLElement} el
159     * @param {Partial<typeof NavigationAMoreList.Defaults>} [defaults]
160     */
161    constructor(el, defaults = {}) {
162        this.ref = new WeakRef(el);
163        const parentClassList = el.parentElement.classList;
164        // Create More Button.
165        const {Templates} = this.constructor;
166
167        this.initalState = Array.from(el.children).map(
168            (el) => new NavigationAMoreMenuItem(el)
169        );
170        const moreElTemplate = parentClassList.contains('drop-down-style--1')
171            ? Templates.style1
172            : parentClassList.contains('drop-down-style--2')
173                ? Templates.style2
174                : Templates.styleDefault;
175        this.more = new class {
176            /** @type {HTMLElement} */
177            el;
178            /** @type {string} */
179            initalClasses;
180            /** @type {string} */
181            initalStyles;
182
183            /**
184             * @param {HTMLElement} el
185             */
186            constructor(el) {
187                this.el = el;
188                this.initalClasses = this.el.getAttribute('class') || '';
189                this.initalStyles = this.el.getAttribute('styles') || '';
190
191                el.querySelector('a.navigation__item.more')?.addEventListener('click', (e) => {
192                    e.preventDefault();
193                });
194            }
195
196            reset() {
197                this.el.setAttribute('class', this.initalClasses);
198                this.el.setAttribute('style', this.initalStyles);
199                this.el.hidden = false;
200            }
201
202            hide() {
203                this.el.hidden = true;
204            }
205        }(moreElTemplate.cloneNode(true).firstElementChild);
206        this.more.hide();
207        el.append(this.more.el);
208
209        this.defaults = {...this.constructor.Defaults, ...defaults};
210    }
211
212    /**
213     * @description arranges the menu's children
214     * @return {void|boolean} false if the html element has lost all references
215     */
216    arrange() {
217        const menuEl = this.el;
218        // Has the element been GCed?
219        if (!menuEl) return false;
220        if (!$(this.el).is(':visible')) return;
221
222        // Pause the observers
223        for (const item of menuEl.children) {
224            menuItemResizeObserver.unobserve(item);
225        }
226
227        const liveItems = [];
228        // Restore initial order state.
229        for (const menuItemState of this.initalState.slice()) {
230            const menuItem = menuItemState.reset();
231            if (menuItem) {
232                liveItems.push(menuItem);
233                continue;
234            }
235
236            // Reset failed because it has been removed. Delete it.
237            const index = this.initalState.indexOf(menuItemState);
238            if (index !== -1) this.initalState.splice(index, 1);
239        }
240
241        // Remove more button, and restore the order
242        menuEl.replaceChildren(...liveItems);
243
244        if (menuEl.children.length) this.tryToFitMore();
245
246        // while adding items to the resize observer, the event fires... we ignore that one time
247        firstMenuItemResizeObserve = true;
248        for (const item of menuEl.children) {
249            menuItemResizeObserver.observe(item, {box: 'border-box'});
250        }
251    }
252
253    static Defaults = {
254          menuWidthBuffer: 4
255        , menuMaxOtherItemsWhenMore: 6
256    };
257    /**
258     * @type {typeof NavigationAMoreList.Defaults}
259     */
260    defaults;
261
262    tryToFitMore() {
263        const menuEl = this.el;
264        const $menu = $(menuEl);
265        const {menuWidthBuffer, menuMaxOtherItemsWhenMore} = this.defaults;
266        const itemsCount = menuEl.children.length;
267        const $closestAlias = $menu.closest("div[class*='alias-']");
268        const availableWidth = ($closestAlias.is("div[class*='alias-partial-header']")) ?
269            Math.floor($menu.closest('.header-partial-navigation').width()) :
270            Math.floor($closestAlias.width());
271        if (availableWidth < menuWidthBuffer) {
272            // if menu width could not be established, leave all items on it, and do not add the More menu
273            // this should never happen, the menu's container should always be resolvable
274            // we're outputting a "soft error" here -- this should be looked at by a developer
275            console.error("Navigation Error: Menu container width could not be established. Skipped adding the More menu.");
276            return;
277        }
278
279        // Check to see if everything is A-OK with the initial state!
280        if (
281            itemsCount <= menuMaxOtherItemsWhenMore
282            && (Math.ceil(calculateMenuWidth($menu)) + menuWidthBuffer) <= availableWidth
283        ) return;
284
285
286        this.more.reset();
287        const moreListEl = this.more.el.querySelector(':scope > ul');
288        const itemsOverflowed = document.createDocumentFragment();
289
290        const menuItems = $menu.children('li:not(.more)');
291        menuEl.append(this.more.el);
292        if ((itemsCount > menuMaxOtherItemsWhenMore)) {
293            // Take the excess items, and detach them from the dom, this is now our starter set
294            itemsOverflowed.append(...menuItems.slice(Math.max(menuMaxOtherItemsWhenMore, 0)).detach().toArray());
295        }
296
297        // Try to make enough room for the more button.
298        if ((Math.ceil(calculateMenuWidth($menu)) + menuWidthBuffer) <= availableWidth) {
299            // Finally append the items that did not fit to the more button.
300            moreListEl?.append(itemsOverflowed);
301            return;
302        }
303        /**
304         * @type {HTMLLIElement[]}
305         */
306        const remainingItems = menuItems.slice(0, menuMaxOtherItemsWhenMore).toArray().reverse();
307        // Check width
308        for (const next of remainingItems) {
309            // Drop next item until there's enough width
310            itemsOverflowed.prepend(next);
311
312            if ((Math.ceil(calculateMenuWidth($menu)) + menuWidthBuffer) <= availableWidth) break;
313            // Try again. still not fitting.
314        }
315        moreListEl?.append(itemsOverflowed);
316    }
317}
318
319function calculateMenuWidth($menu) {
320    let width = 0;
321    $menu.children().each(function (i) {
322        width += Math.ceil($(this).outerWidth(true));
323    });
324
325    return width;
326}
327
328/**
329 *
330 * @param {number} maxWidth
331 * @param {HTMLElement} el
332 * @return {number}
333 */
334const largestWidth = (maxWidth, el) => {
335    const width = $(el).outerWidth(true);
336    if (width > maxWidth) return width;
337    return maxWidth;
338};
339
340class MegaMenu {
341    /**
342     * @param {NodeListOf<HTMLElement>} [menuList]
343     */
344    static reposition(menuList) {
345        menuList ??= document.querySelectorAll(
346            '.navigation__item.mega-menu > ul, .navigation-b .navigation__menu--container, .navigation-c .navigation__menu--container'
347        );
348        const windowWidth = $(window).width();
349        for (const el of menuList) this._position(el, windowWidth);
350    }
351
352    /**
353     *
354     * @param {HTMLElement} el
355     * @param {number} windowWidth
356     * @protected
357     */
358    static _position(el, windowWidth) {
359        const el$ref = $(el);
360        let itemOffset = 0;
361        if (el.parentElement?.classList.contains('mega-menu')) {
362            itemOffset = 15;
363        }
364
365        // Get the left position of the module container.
366        const parentOffset = el$ref.closest('[class*="alias-"]').offset().left;
367
368        /* setting the width of the flyout to be the width of the window. max-width rule from the css stops it to become larger than 1140px
369        On mobile and tablet and partialy on desktop it will be from edge to edge */
370        el$ref.width(windowWidth);
371
372        /* the magic. the left position is always the widht of the window minus the current width of the flyout divided by 2,
373        and then substruct the flyouts parent offset and add 15px (15px is the padding of the parent)*/
374        el$ref.css('left', (((windowWidth - el$ref.width()) / 2) - parentOffset + itemOffset));
375    }
376}
377
378
379class NavigationAMoreMenuItem {
380    /** @type {WeakRef<HTMLElement>} */
381    ref;
382    /** @type {string} */
383    classes;
384    /** @type {string} */
385    styles;
386
387    /**
388     * @param {HTMLElement} el
389     */
390    constructor(el) {
391        this.ref = new WeakRef(el);
392        this.classes = el.getAttribute('class') || '';
393        this.styles = el.getAttribute('style') || '';
394    }
395
396    get el() {
397        return this.ref.deref();
398    }
399
400    reset() {
401        const el = this.el;
402        if (!el) return false;
403        el.setAttribute('class', this.classes);
404        el.setAttribute('style', this.styles);
405        return el;
406    }
407}
408
409class NavigationA {
410
411    /**
412     * @type {WeakSet<HTMLElement>}
413     */
414    static initialized = new WeakSet();
415
416    static initializeAll() {
417        const navigationAList = document.querySelectorAll(
418            '.component.navigation .navigation-a'
419        );
420        for (const navA of navigationAList) {
421            this.initialize(navA);
422        }
423        return this;
424    }
425
426    static initialize(navA) {
427        // Don't add duplicate event listeners
428        if (this.initialized.has(navA)) return;
429        this.initialized.add(navA);
430
431        const navA$ref = $(navA);
432        // FIXME: This code does not provide any means of managing the menus in desktop mode or tablet mode to produce keyboard navigation.
433
434        const expandDropDown = (el) => {
435            $(el).toggleClass('expanded').parent().next('ul').slideToggle();
436        };
437        /**
438         * @param {IJQueryEvent<MouseEvent>} e
439         */
440        const onExpandMore = (e) => {
441            e.preventDefault();
442            e.stopImmediatePropagation();
443            // Trigger the child expand more element (run onExpand)
444            expandDropDown(e.currentTarget.querySelector(':scope > .navigation__item--expand'));
445        };
446
447        /**
448         * @param {IJQueryEvent<MouseEvent>} e
449         */
450        const onExpandClick = (e) => {
451            e.preventDefault();
452            e.stopImmediatePropagation();
453            expandDropDown(e.currentTarget);
454        };
455
456        /** @type {(()=>void)[]} */
457        const mediaResizeDestructors = [];
458        /**
459         * @param {MediaQueryListEvent|MediaQueryList} event
460         */
461        const onMediaChange = (event) => {
462            for (const fn of mediaResizeDestructors.splice(0, mediaResizeDestructors.length)) try {
463                fn();
464            } catch (e) {
465                console.error(e);
466            }
467
468            if (!document.contains(navA)) {
469                megaMenuBreakpoint.removeEventListener('change', onMediaChange);
470                return;
471            }
472
473            // document.documentElement.clientWidth <= 992
474            const target$ref = event.matches ? navA$ref : navA$ref.find('.mega-menu');
475            target$ref.on('click', '.navigation__item--expand_more', onExpandMore);
476            target$ref.on('click', 'a.navigation__item--link .navigation__item--expand', onExpandClick);
477            mediaResizeDestructors.push(() => {
478                target$ref.off('click', '.navigation__item--expand_more', onExpandMore);
479                target$ref.off('click', 'a.navigation__item--link .navigation__item--expand', onExpandClick);
480
481                // slideToggle sets the display to block/none on expand/hide
482                // remove the CSS display value, so the element will use the default value specified in theme
483                target$ref.find('.navigation__list--sub').css('display', '');
484            });
485        };
486
487        megaMenuBreakpoint.addEventListener('change', onMediaChange);
488        onMediaChange(megaMenuBreakpoint);
489
490        // If this is expected to have more, then we need to add a subscription on reposition by adding to the `navAMenuList`
491        if (navA.dataset.moreButton === '1') {
492            /**
493             * @type {NodeListOf<HTMLElement>}
494             */
495            const navAWithMoreList = navA.querySelectorAll('.navigation__list.more_nav:not(.mm-listview)');
496            for (const el of navAWithMoreList) {
497                // Hook in the Behavior for the repositioning
498                NavigationAMoreList.create(el);
499            }
500        }
501
502        // Drop down adjustments
503        /* Add sub item class if too close to edges */
504        /**
505         * @param {IJQueryEvent<MouseEvent>}  event
506         */
507        const onHover = (event) => {
508            const el = event.currentTarget;
509            const $this = $(el);
510            const $navListChildItem = $this.children('.navigation__list--sub');
511            const $navListChildSubItem = $navListChildItem.find('> .navigation__item.has-drop-down > .navigation__list--sub');
512            const $navListGrandChildSubItem = $navListChildSubItem.find('> .navigation__item.has-drop-down > .navigation__list--sub');
513            const widthChildItem = $navListChildItem.outerWidth(true);
514            const navListSub_offsetLeft = $this.offset().left;
515            const buffer = 60;
516
517            const maxWidthChildSubItem = $navListChildSubItem.toArray().reduce(largestWidth, 0);
518            const maxWidthGrandChildSubItem = $navListGrandChildSubItem.toArray().reduce(largestWidth, 0);
519
520            const navListSub_width = (widthChildItem + maxWidthChildSubItem + maxWidthGrandChildSubItem) - $this.outerWidth(true);
521
522            const offsetRight = $(document).width() - (navListSub_offsetLeft + $this.outerWidth(true) + buffer);
523            const isEntirelyVisible = (offsetRight >
523 navListSub_width);
524
525            el.classList.toggle('near-edge', !isEntirelyVisible);
526        };
527
528        // feature/SMBWMGR-14028: .navigation-a > .navigation__list > .navigation__item.has-drop-down
529        navA$ref.children('.navigation__list').on(
530              'mouseenter mouseleave'
531            , '> .navigation__item.has-drop-down'
532            , onHover
533        );
534    }
535}
536
537// TODO: Manage State
538class NavigationOther {
539    static initializeAll() {
540        const navigationOtherList = document.querySelectorAll(
541            '.component.navigation .navigation-b, .component.navigation .navigation-c'
542        );
543        for (const el of navigationOtherList) {
544            this.initialize(el);
545        }
546    }
547
548    static initialize(nav) {
549        const navButton = $(nav).children('.navigation__button');
550        const navMenu = $(nav).children('.navigation__menu--container');
551        const expandButton = $(nav).find('.navigation__item--expand');
552
553        navButton.click(function() {
554            if (!(navButton.hasClass('animating'))) {
555                navButton.addClass('animating');
556                navButton.children('.navigation__button--icon').toggleClass('is-active');
557
558                if (!(navMenu.hasClass('animating'))) {
559                    navMenu.addClass('animating');
560                    navMenu.toggleClass('expanded');
561                    setTimeout(function(){
562                        navMenu.removeClass('animating');
563                        navButton.removeClass('animating');
564                    }, 500);
565                }
566            }
567
568        });
569
570        $(document).click(function(e) {
571            if ((!navMenu.is(e.target) && navMenu.has(e.target).length === 0) && (!navButton.is(e.target) && navButton.has(e.target).length === 0)) {
572                navButton.children('.navigation__button--icon').removeClass('is-active');
573                if (!(navMenu.hasClass('animating'))) {
574                    navMenu.addClass('animating');
575                    navMenu.removeClass('expanded');
576                    setTimeout(function(){
577                        navMenu.removeClass('animating');
578                        navButton.removeClass('animating');
579                    }, 500);
580                }
581            }
582        });
583
584        expandButton.click(function() {
585            $(this).toggleClass('expanded').next().slideToggle();
586        });
587    }
588}
589
590$(() => {
591    //sticky nav
592    $('[data-sticky-header]').closest('.js-sticky-nav').sticky({topSpacing: 0});
593
594
595    NavigationOther.initializeAll();
596    NavigationAMoreList.unmask();
597    NavigationA.initializeAll();
598
599    addMMenuTitleAttributes();
600
601    positionNavs();
602
603    window.addEventListener('resize', () => {
604        schedulePositionNavs();
605    });
606});
607})(window.jQuery);

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.