PageSourceSearch

https://www.tokai-techno.co.jp/cms/wp-content/themes/common-assets/js/nav.js

js tokai-techno.co.jp collected 2026-10-04 09:24:39 UTC 18,334 bytes, 581 lines download raw bytes

1class UnifiedAccessibleMegaMenu {
2  constructor(config = {}) {
3    // デフォルト設定
4    this.config = {
5      selectors: {
6        globalNav: '.global-nav',
7        header: '.header',
8        mobileMenuToggle: '.mobile-menu-toggle',
9        mobileNavOverlay: '.mobile-nav-overlay',
10        menuOverlay: '.menu-overlay',
11        globalNavSub: '.global-nav-sub',
12        grandchildNav: '.grandchild-nav',
13        megaMenuContent: '.mega-menu-content',
14        megaMenuInner: '.mega-menu-inner',
15        megaMenuSubNav: '.mega-menu-sub-nav',
16        megaMenuGrandchild: '.mega-menu-grandchild',
17        megaMenuMessage: '.mega-menu-message'
18      },
19      classes: {
20        isOpen: 'is-open',
21        isActive: 'is-active',
22        scrollLock: 'scroll-lock',
23        isIOS: 'is-ios',
24        singleColumn: 'single-column'
25      },
26      breakpoint: 768,
27      ...config
28    };
29
30    this.isMobile = false;
31    this.originalScrollPosition = 0;
32    this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
33    this.init();
34  }
35
36  // ヘルパーメソッド: セレクタを取得
37  getSelector(key) {
38    return this.config.selectors[key];
39  }
40
41  // ヘルパーメソッド: クラス名を取得
42  getClass(key) {
43    return this.config.classes[key];
44  }
45
46  init() {
47    this.checkDevice();
48    this.cloneMobileNav();
49    this.setupEventListeners();
50    this.setupKeyboardNavigation();
51
52    window.addEventListener('resize', () => {
53      this.checkDevice();
54      if (!this.isMobile) {
55        this.closeMobileMenu();
56      }
57    });
58  }
59
60  checkDevice() {
61    this.isMobile = window.innerWidth <= this.config.breakpoint;
62  }
63
64  cloneMobileNav() {
65    const originalNav = document.querySelector(this.getSelector('globalNav'));
66    const mobileOverlay = document.querySelector(this.getSelector('mobileNavOverlay'));
67    const clonedNav = originalNav.cloneNode(true);
68    mobileOverlay.appendChild(clonedNav);
69  }
70
71  setupEventListeners() {
72    // Main menu buttons (original and cloned)
73    document.querySelectorAll(`${this.getSelector('globalNav')} > li > button`).forEach(button => {
74      button.addEventListener('click', (e) => {
75        e.preventDefault();
76        this.handleMainMenuClick(e.target);
77      });
78    });
79
80    // Sub menu buttons (original and cloned)
81    document.querySelectorAll(`${this.getSelector('globalNav')} ${this.getSelector('globalNavSub')} > li > button`).forEach(button => {
82      button.addEventListener('click', (e) => {
83        e.preventDefault();
84        this.handleSubMenuClick(e.target);
85      });
86    });
87
88    // Mobile menu toggle
89    const mobileToggle = document.querySelector(this.getSelector('mobileMenuToggle'));
90    if (mobileToggle) {
91      mobileToggle.addEventListener('click', () => {
92        this.toggleMobileMenu();
93      });
94    }
95
96    // Close overlays when clicking outside
97    document.querySelector(this.getSelector('menuOverlay')).addEventListener('click', (e) => {
98      if (e.target === e.currentTarget) {
99        this.closeAllPCMenus();
100      }
101    });
102
103    // Close menus when clicking outside navigation area
104    document.addEventListener('click', (e) => {
105      const nav = e.target.closest(`${this.getSelector('header')} ${this.getSelector('globalNav')}`);
106      const menuOverlay = e.target.closest(this.getSelector('menuOverlay'));
107
108      if (!nav && !menuOverlay) {
109        this.closeAllPCMenus();
110      }
111    });
112  }
113
114  saveScrollPosition() {
115    this.originalScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
116  }
117
118  lockScroll() {
119    this.saveScrollPosition();
120    document.body.classList.add(this.getClass('scrollLock'));
121
122    if (this.isIOS) {
123      document.body.classList.add(this.getClass('isIOS'));
124      document.body.style.top = `-${this.originalScrollPosition}px`;
125    }
126
127    document.addEventListener('touchmove', this.preventTouchMove, { passive: false });
128  }
129
130  unlockScroll() {
131    document.body.classList.remove(this.getClass('scrollLock'));
132
133    if (this.isIOS) {
134      document.body.classList.remove(this.getClass('isIOS'));
135      document.body.style.top = '';
136      window.scrollTo(0, this.originalScrollPosition);
137    }
138
139    document.removeEventListener('touchmove', this.preventTouchMove);
140  }
141
142  preventTouchMove(e) {
143    const scrollableElements = e.target.closest('.mega-menu-content, .mobile-nav-overlay');
144    if (!scrollableElements) {
145      e.preventDefault();
146    }
147  }
148
149  handleMainMenuClick(button) {
150    const li = button.parentElement;
151    const isHeaderNav = li.closest(this.getSelector('header'));
152
153    if (this.isMobile || !isHeaderNav) {
154      this.toggleSubMenu(li);
155    } else {
156      this.handlePCMenu(li);
157    }
158  }
159
160  handlePCMenu(li) {
161    const subNav = li.querySelector(this.getSelector('globalNavSub'));
162    if (!subNav) return;
163
164    const isCurrentlyOpen = li.classList.contains(this.getClass('isOpen'));
165
166    if (isCurrentlyOpen) {
167      this.closeAllPCMenus();
168    } else {
169      this.closeAllPCMenus();
170      this.openPCMenu(li, subNav);
171    }
172  }
173
174  openPCMenu(li, subNav) {
175    li.classList.add(this.getClass('isOpen'));
176    li.querySelector('button').setAttribute('aria-expanded', 'true');
177
178    const menuOverlay = document.querySelector(this.getSelector('menuOverlay'));
179    const megaMenuContent = document.querySelector(this.getSelector('megaMenuContent'));
180    const megaMenuSubNav = document.querySelector(this.getSelector('megaMenuSubNav'));
181    const megaMenuGrandchild = document.querySelector(this.getSelector('megaMenuGrandchild'));
182    const megaMenuMessage = document.querySelector(this.getSelector('megaMenuMessage'));
183
184    megaMenuSubNav.innerHTML = '';
185    megaMenuGrandchild.innerHTML = '';
186
187    const subItems = subNav.children;
188    let hasGrandchildren = false;
189    let firstItemWithGrandchild = null;
190    let firstGrandchildNav = null;
191
192    Array.from(subItems).forEach((item, index) => {
193      const clonedItem = item.cloneNode(true);
194      const grandchildNav = item.querySelector(this.getSelector('grandchildNav'));
195
196      if (grandchildNav && grandchildNav.children.length > 0) {
197        hasGrandchildren = true;
198        clonedItem.addEventListener('mouseenter', () => {
199          this.showMegaMenuGrandchildren(clonedItem);
200        });
201
202        if (!firstItemWithGrandchild) {
203          firstItemWithGrandchild = clonedItem;
204          firstGrandchildNav = grandchildNav;
205        }
206      } else {
207        clonedItem.addEventListener('mouseenter', () => {
208          this.clearMegaMenuGrandchildren();
209          megaMenuSubNav.querySelectorAll('li').forEach(sibling => {
210            sibling.classList.remove(this.getClass('isActive'));
211          });
212          clonedItem.classList.add(this.getClass('isActive'));
213        });
214      }
215
216      megaMenuSubNav.appendChild(clonedItem);
217    });
218
219    if (hasGrandchildren) {
220      megaMenuContent.classList.remove(this.getClass('singleColumn'));
221      megaMenuMessage.style.display = 'none';
222      megaMenuGrandchild.style.display = 'grid';
223
224      if (firstItemWithGrandchild && firstGrandchildNav) {
225        firstItemWithGrandchild.classList.add(this.getClass('isActive'));
226        this.populateMegaMenuGrandchildren(firstGrandchildNav);
227      }
228    } else {
229      megaMenuContent.classList.add(this.getClass('singleColumn'));
230      megaMenuGrandchild.style.display = 'none';
231      megaMenuMessage.style.display = 'none';
232    }
233
234    menuOverlay.classList.add(this.getClass('isOpen'));
235  }
236
237  showMegaMenuGrandchildren(li) {
238    const grandchildNav = li.querySelector(this.getSelector('grandchildNav'));
239    const megaMenuMessage = document.querySelector(this.getSelector('megaMenuMessage'));
240    const megaMenuGrandchild = document.querySelector(this.getSelector('megaMenuGrandchild'));
241
242    li.parentElement.querySelectorAll('li').forEach(sibling => {
243      sibling.classList.remove(this.getClass('isActive'));
244    });
245    li.classList.add(this.getClass('isActive'));
246
247    if (grandchildNav && grandchildNav.children.length > 0) {
248      megaMenuMessage.style.display = 'none';
249      megaMenuGrandchild.style.display = 'grid';
250      this.populateMegaMenuGrandchildren(grandchildNav);
251    } else {
252      this.clearMegaMenuGrandchildren();
253    }
254  }
255
256  populateMegaMenuGrandchildren(grandchildNav) {
257    const megaMenuGrandchild = document.querySelector(this.getSelector('megaMenuGrandchild'));
258    megaMenuGrandchild.innerHTML = '';
259
260    if (!grandchildNav) return;
261
262    Array.from(grandchildNav.children).forEach(item => {
263      const clonedItem = item.cloneNode(true);
264      megaMenuGrandchild.appendChild(clonedItem);
265    });
266  }
267
268  clearMegaMenuGrandchildren() {
269    const megaMenuGrandchild = document.querySelector(this.getSelector('megaMenuGrandchild'));
270    const megaMenuMessage = document.querySelector(this.getSelector('megaMenuMessage'));
271
272    megaMenuGrandchild.innerHTML = '';
273    megaMenuGrandchild.style.display = 'none';
274    megaMenuMessage.style.display = 'flex';
275  }
276
277  closeAllPCMenus() {
278    document.querySelectorAll(`${this.getSelector('header')} ${this.getSelector('globalNav')} > li.${this.getClass('isOpen')}`).forEach(item => {
279      this.closeMenuItem(item);
280    });
281
282    const menuOverlay = document.querySelector(this.getSelector('menuOverlay'));
283    if (menuOverlay.classList.contains(this.getClass('isOpen'))) {
284      menuOverlay.classList.remove(this.getClass('isOpen'));
285    }
286  }
287
288  closePCMenu() {
289    this.closeAllPCMenus();
290  }
291
292  closeSiblingMobileMenus(currentLi) {
293    const isInMobileOverlay = currentLi.closest(this.getSelector('mobileNavOverlay'));
294    if (!isInMobileOverlay) return;
295
296    const parentUl = currentLi.parentElement;
297
298    Array.from(parentUl.children).forEach(siblingLi => {
299      if (siblingLi !== currentLi && siblingLi.classList.contains(this.getClass('isOpen'))) {
300        this.closeMenuItem(siblingLi);
301      }
302    });
303  }
304
305  closeSiblingMobileSubMenus(currentLi) {
306    const isInMobileOverlay = currentLi.closest(this.getSelector('mobileNavOverlay'));
307    if (!isInMobileOverlay) return;
308
309    const parentUl = currentLi.parentElement;
310
311    Array.from(parentUl.children).forEach(siblingLi => {
312      if (siblingLi !== currentLi && siblingLi.classList.contains(this.getClass('isOpen'))) {
313        this.closeSubMenuItem(siblingLi);
314      }
315    });
316  }
317
318  toggleSubMenu(li) {
319    const subNav = li.querySelector(this.getSelector('globalNavSub'));
320    const button = li.querySelector('button');
321
322    if (!subNav || !button) return;
323
324    const isOpen = li.classList.contains(this.getClass('isOpen'));
325
326    if (isOpen) {
327      this.closeMenuItem(li);
328    } else {
329      this.closeSiblingMobileMenus(li);
330      this.openMenuItem(li);
331    }
332  }
333
334  handleSubMenuClick(button) {
335    const li = button.parentElement;
336    const grandchildNav = li.querySelector(this.getSelector('grandchildNav'));
337
338    if (!grandchildNav) return;
339
340    const isOpen = li.classList.contains(this.getClass('isOpen'));
341
342    if (isOpen) {
343      this.closeSubMenuItem(li);
344    } else {
345      this.closeSiblingMobileSubMenus(li);
346      this.openSubMenuItem(li);
347    }
348  }
349
350  openMenuItem(li) {
351    li.classList.add(this.getClass('isOpen'));
352    const subNav = li.querySelector(this.getSelector('globalNavSub'));
353    const button = li.querySelector('button');
354
355    if (subNav) {
356      subNav.classList.add(this.getClass('isOpen'));
357
358      const overlay = document.querySelector(this.getSelector('mobileNavOverlay'));
359      if (overlay) {
360        const hasGrandchild = subNav.querySelector(this.getSelector('grandchildNav'));
361        if (hasGrandchild) {
362          overlay.classList.remove(this.getClass('singleColumn'));
363        } else {
364          overlay.classList.add(this.getClass('singleColumn'));
365        }
366      }
367    }
368
369    if (button) button.setAttribute('aria-expanded', 'true');
370  }
371
372  closeMenuItem(li) {
373    li.classList.remove(this.getClass('isOpen'));
374    const subNav = li.querySelector(this.getSelector('globalNavSub'));
375    const button = li.querySelector('button');
376
377    if (subNav) subNav.classList.remove(this.getClass('isOpen'));
378    if (button) button.setAttribute('aria-expanded', 'false');
379
380    li.querySelectorAll(`${this.getSelector('globalNavSub')} li.${this.getClass('isOpen')}`).forEach(subItem => {
381      this.closeSubMenuItem(subItem);
382    });
383  }
384
385  openSubMenuItem(li) {
386    li.classList.add(this.getClass('isOpen'));
387    const grandchildNav = li.querySelector(this.getSelector('grandchildNav'));
388    const button = li.querySelector('button');
389
390    if (grandchildNav) grandchildNav.classList.add(this.getClass('isOpen'));
391    if (button) button.setAttribute('aria-expanded', 'true');
392  }
393
394  closeSubMenuItem(li) {
395    li.classList.remove(this.getClass('isOpen'));
396    const grandchildNav = li.querySelector(this.getSelector('grandchildNav'));
397    const button = li.querySelector('button');
398
399    if (grandchildNav) grandchildNav.classList.remove(this.getClass('isOpen'));
400    if (button) button.setAttribute('aria-expanded', 'false');
401  }
402
403  toggleMobileMenu() {
404    const mobileToggle = document.querySelector(this.getSelector('mobileMenuToggle'));
405    const mobileOverlay = document.querySelector(this.getSelector('mobileNavOverlay'));
406
407    const isOpen = mobileOverlay.classList.contains(this.getClass('isOpen'));
408
409    if (isOpen) {
410      this.closeMobileMenu();
411    } else {
412      this.openMobileMenu();
413    }
414  }
415
416  openMobileMenu() {
417    const mobileToggle = document.querySelector(this.getSelector('mobileMenuToggle'));
418    const mobileOverlay = document.querySelector(this.getSelector('mobileNavOverlay'));
419
420    mobileToggle.classList.add(this.getClass('isOpen'));
421    mobileOverlay.classList.add(this.getClass('isOpen'));
422    mobileToggle.setAttribute('aria-expanded', 'true');
423    this.lockScroll();
424  }
425
426  closeMobileMenu() {
427    const mobileToggle = document.querySelector(this.getSelector('mobileMenuToggle'));
428    const mobileOverlay = document.querySelector(this.getSelector('mobileNavOverlay'));
429
430    mobileToggle.classList.remove(this.getClass('isOpen'));
431    mobileOverlay.classList.remove(this.getClass('isOpen'));
432    mobileToggle.setAttribute('aria-expanded', 'false');
433    this.unlockScroll();
434
435    mobileOverlay.querySelectorAll(`.${this.getClass('isOpen')}`).forEach(item => {
436      item.classList.remove(this.getClass('isOpen'));
437      const button = item.querySelector('button');
438      if (button) {
439        button.setAttribute('aria-expanded', 'false');
440      }
441      const subNav = item.querySelector(`${this.getSelector('globalNavSub')}, ${this.getSelector('grandchildNav')}`);
442      if (subNav) {
443        subNav.classList.remove(this.getClass('isOpen'));
444      }
445    });
446  }
447
448  setupKeyboardNavigation() {
449    document.addEventListener('keydown', (e) => {
450      if (e.key === 'Escape') {
451        this.closeAllPCMenus();
452        this.closeMobileMenu();
453      }
454    });
455
456    const focusableElements = 'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])';
457
458    document.addEventListener('keydown', (e) => {
459      if (e.key === 'Tab') {
460        const menuOverlay = document.querySelector(`${this.getSelector('menuOverlay')}.${this.getClass('isOpen')}`);
461        const mobileOverlay = document.querySelector(`${this.getSelector('mobileNavOverlay')}.${this.getClass('isOpen')}`);
462
463        if (menuOverlay) {
464          this.handleTabNavigation(e, menuOverlay, focusableElements);
465        } else if (mobileOverlay) {
466          this.handleTabNavigation(e, mobileOverlay, focusableElements);
467        }
468      }
469    });
470  }
471
472  handleTabNavigation(e, container, focusableElements) {
473    const focusable = container.querySelectorAll(focusableElements);
474    const firstFocusable = focusable[0];
475    const lastFocusable = focusable[focusable.length - 1];
476
477    if (e.shiftKey) {
478      if (document.activeElement === firstFocusable) {
479        e.preventDefault();
480        lastFocusable.focus();
481      }
482    } else {
483      if (document.activeElement === lastFocusable) {
484        e.preventDefault();
485        firstFocusable.focus();
486      }
487    }
488  }
489}
490
491// 使用例1: デフォルト設定で初期化
492document.addEventListener('DOMContentLoaded', () => {
493  new UnifiedAccessibleMegaMenu();
494});
495
496// 使用例2: カスタム設定で初期化
497// document.addEventListener('DOMContentLoaded', () => {
498//   new UnifiedAccessibleMegaMenu({
499//     selectors: {
500//       globalNav: '.custom-nav',
501//       header: '.custom-header',
502//       mobileMenuToggle: '.custom-toggle',
503//       // 他のセレクタも同様にカスタマイズ可能
504//     },
505//     classes: {
506//       isOpen: 'active',
507//       isActive: 'current',
508//       // 他のクラス名も同様にカスタマイズ可能
509//     },
510//     breakpoint: 1024
511//   });
512// });
513
514
515// 字詰め
516$(function () {
517  const targetFonts = [
518    'Noto Sans CJK JP',
519    'Noto Sans JP',
520  ];
521
522  // クラスごとに対象文字を定義
523  const symbolRules = [
524    {
525      className: 'tight-full-open',
526      symbols: ['(', '「',  '『', '【', '[', '{']
527    },
528    {
529      className: 'tight-full-close',
530      symbols: [')', '」', '』', '】', ']', '}']
531    },
532    {
533      className: 'tight-half-open',
534      symbols: ['〈', '《', '〔']
535    },
536    {
537      className: 'tight-half-close',
538      symbols: ['〉', '》', '〕']
539    },
540    {
541      className: 'tight-symbol',
542      symbols: ['・', '、', '。', ':']
543    },
544  ];
545
546  function wrapSymbol(char, className) {
547    if (char.match(/^<span[^>]*>.*<\/span>$/)) {
548      return char.replace(/class="([^"]*)"/, 'class="$1 ' + className + '"');
549    }
550    return `<span class="${className}">${char}</span>`;
551  }
552
553  $('header *').contents().filter(function () {
554    return this.nodeType === 3 && $.trim(this.nodeValue).length > 0;
555  }).each(function () {
556    const $parent = $(this).parent();
557    const fontFamily = $parent.css('font-family') || '';
558    const hasTargetFont = targetFonts.some(font => fontFamily.includes(font));
559    if (!hasTargetFont) return;
560
561    let text = $(this).text();
562    let result = '';
563
564    for (let i = 0; i < text.length; i++) {
565      const char = text[i];
566      let wrapped = char;
567
568      for (const rule of symbolRules) {
569        if (rule.symbols.includes(char)) {
570          wrapped = wrapSymbol(char, rule.className);
571          break; // 最初にマッチしたルールを適用
572        }
573      }
574
575      result += wrapped;
576    }
577
578    $(this).replaceWith(result);
579  });
580});
581

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.