PageSourceSearch

https://www.gsscloud.com/templates/vital2022/js/modules/nav-produc…?b63ae3952263319d4e7755155824cb94

js gsscloud.com collected 2026-09-30 22:00:12 UTC 7,265 bytes, 167 lines download raw bytes

1document.addEventListener('DOMContentLoaded', () => {
2  // 監聽範圍是 .nav__container,不是 .nav__list:.nav__login(立即登入/會員選單面板)
3  // 跟 burger 平行放在 .nav__list 外面,click 事件不會 bubble 到 .nav__list 上,
4  // 兩者共同的父層 .nav__container 才能同時涵蓋到。
5  const navContainer = document.querySelector('.nav__container');
6  if (!navContainer) return;
7
8  // 讀 _rwd.scss 輸出的 --bp-laptop-m,跟 SCSS 的 rwd(laptop_M) 共用同一個斷點來源
9  const bpLaptopM =
10    getComputedStyle(document.documentElement).getPropertyValue('--bp-laptop-m').trim() || '1024px';
11  const mqMobile = window.matchMedia(`(max-width: ${bpLaptopM})`);
12  const mqReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
13
14  // 鎖定 .push-view-host.panel-open(複合選擇器),不要查裸的 .panel-open——
15  // burger 的 .nav.panel-open 是另一個獨立狀態,混在一起清會誤關 burger drawer。
16  const nav = document.querySelector('.nav');
17
18  // inert 跟「mobile 斷點內 + host 是否 panel-open」同步,不只在 click 時切換——
19  // 頁面剛載入或從 desktop resize 進 mobile 時都還沒點過東西,drawer 視覺上已收合
20  // 卻從沒被設過 inert(見 _user-menu.scss 的說明)。
21  const syncLoginDrawerInert = () => {
22    document.querySelectorAll('.push-view-host').forEach((host) => {
23      const drawer = host.querySelector(':scope > .nav__login__drawer');
24      if (!drawer) return;
25      drawer.toggleAttribute('inert', mqMobile.matches && !host.classList.contains('panel-open'));
26    });
27  };
28  syncLoginDrawerInert();
29
30  // 關閉時順手清掉 .nav__login__drawer 的 inert(其他 host 沒有這個子層,no-op)
31  const closeAllPanels = () => {
32    document.querySelectorAll('.push-view-host.panel-open').forEach((el) => {
33      el.classList.remove('panel-open');
34      el.querySelector(':scope > .nav__login__drawer')?.setAttribute('inert', '');
35    });
36  };
37
38  const navList = document.querySelector('.nav__list');
39  const openPanel = (panelEl) => {
40    closeAllPanels();
41    // .nav__login 跟 burger drawer 是平行的兩個獨立 overlay:開它之前如果 drawer
42    // 還開著,借用 burger 自己的 click handler 關掉(不重複寫一份清理邏輯)
43    if (navList && !navList.contains(panelEl) && nav && nav.classList.contains('panel-open')) {
44      document.querySelector('.burger')?.click();
45    }
46    panelEl.classList.add('panel-open');
47    panelEl.querySelector(':scope > .nav__login__drawer')?.removeAttribute('inert');
48  };
49
50  // 切換 product-dropdown tab 的 active 狀態(desktop / mobile 共用)
51  const activateTab = (tabEl) => {
52    const root = tabEl.closest('.product-dropdown');
53    if (!root) return;
54    const alias = tabEl.dataset.tag;
55    const prevHeight = root.offsetHeight;
56
57    root
58      .querySelectorAll('.product-dropdown__tab')
59      .forEach((t) => t.classList.toggle('is-active', t === tabEl));
60    root
61      .querySelectorAll('.npd-panel')
62      .forEach((p) => p.classList.toggle('is-active', p.dataset.panel === alias));
63
64    // Desktop 切 tab 時做高度過渡動畫
65    if (!mqMobile.matches && !mqReducedMotion.matches) {
66      const newHeight = root.offsetHeight;
67      if (prevHeight !== newHeight) {
68        root.animate([{ height: `${prevHeight}px` }, { height: `${newHeight}px` }], {
69          duration: 300,
70          easing: 'ease',
71          fill: 'none',
72        });
73      }
74    }
75  };
76
77  navContainer.addEventListener('click', (e) => {
78    // 共用返回(任何情況都生效,desktop 也允許 — 雖然 desktop .nav__back 看不見)
79    if (e.target.closest('.nav__back')) {
80      closeAllPanels();
81      return;
82    }
83
84    // .nav__login 自己的 ✕ 關閉鈕(跟 .nav__back 分開,不共用「返回」語意)
85    if (e.target.closest('.nav__login__close')) {
86      closeAllPanels();
87      return;
88    }
89
90    // product-dropdown tab:desktop 純切 tab;mobile 還要走 push-view 流程
91    const tab = e.target.closest('.product-dropdown__tab');
92    if (tab) {
93      activateTab(tab);
94      if (mqMobile.matches) {
95        openPanel(tab.closest('.push-view-host'));
96      }
97      return;
98    }
99
100    // Mobile-only:點在 .push-view-host 內、但不在「host 自己的」panel 內才觸發 openPanel
101    // (panel 內的連結要照常導航)。用 host.contains(panel) 而非「有 .push-view-panel 祖先」,
102    // 因為 .menu-main 最外層 <ul> 也帶這個 class,對它自己的 L1 <li> 是祖先不是子孫,
103    // 判斷法會誤判。同時要認 .push-view-panel--dropdown:.nav__login 的 mobile 面板只有
104    // 這個 class(沒有 .push-view-panel),不認的話點面板內非連結區域會再誤觸發一次。
105    if (!mqMobile.matches) return;
106    const host = e.target.closest('.push-view-host');
107    if (!host) return;
108    const panel = e.target.closest('.push-view-panel, .push-view-panel--dropdown');
109    if (panel && host.contains(panel)) return;
110    openPanel(host);
111  });
112
113  // <li role="tab"> 非原生互動元素,瀏覽器不會自動把 Enter/Space 轉成 click
114  navContainer.addEventListener('keydown', (e) => {
115    if (e.key !== 'Enter' && e.key !== ' ') return;
116    const tab = e.target.closest('.product-dropdown__tab');
117    if (!tab) return;
118    e.preventDefault();
119    activateTab(tab);
120    if (mqMobile.matches) {
121      openPanel(tab.closest('.push-view-host'));
122    }
123  });
124
125  // .product-dropdown 是獨立模組、DOM 位置在觸發器之前,自然 Tab 順序接不上,需手動導:
126  // 進面板/面板尾出去/Shift+Tab 回觸發器——少了「面板尾出去」會被第一點導回面板出不去
127  navContainer.addEventListener('keydown', (e) => {
128    if (e.key !== 'Tab' || mqMobile.matches) return;
129
130    const trigger = document.querySelector('.nav-header.nav-product');
131    const dropdown = document.querySelector('.product-dropdown');
132    if (!trigger || !dropdown) return;
133
134    if (e.target === trigger && !e.shiftKey) {
135      const focusable = dropdown.querySelectorAll('a[href], [tabindex]');
136      e.preventDefault();
137      focusable[0]?.focus();
138      return;
139    }
140
141    if (!dropdown.contains(e.target)) return;
142    const focusable = [...dropdown.querySelectorAll('a[href], [tabindex]')].filter(
143      (el) => el.tabIndex !== -1 && el.offsetParent !== null,
144    );
145
146    if (!e.shiftKey && e.target === focusable[focusable.length - 1]) {
147      // 沒有下一個項目可以接手時(例如 trigger 被移到主選單最後一項)就不 preventDefault,
148      // 讓瀏覽器走原生 Tab 行為,避免焦點被攔住卻無處可去
149      const next = trigger
150        .closest('.nav-item')
151        ?.nextElementSibling?.querySelector(':scope > a, :scope > .nav-header');
152      if (next) {
153        e.preventDefault();
154        next.focus();
155      }
156    } else if (e.shiftKey && e.target === focusable[0]) {
157      e.preventDefault();
158      trigger.focus();
159    }
160  });
161
162  // .nav(burger drawer)的 panel-open/inert 由 global.js 自己管,不歸這裡(.nav 不是 push-view-host)
163  mqMobile.addEventListener('change', (e) => {
164    if (!e.matches) closeAllPanels();
165    syncLoginDrawerInert();
166  });
167});

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.