PageSourceSearch

https://medeco.io/js/main.js?v=8

js medeco.io collected 2026-10-02 05:15:50 UTC 23,266 bytes, 711 lines download raw bytes

1'use strict';
2
3/* =========================================
4   Header: スクロール時スタイル切替
5   ========================================= */
6(function () {
7  const header = document.querySelector('.site-header');
8  if (!header) return;
9
10  function updateHeader() {
11    if (window.scrollY > 10) {
12      header.classList.add('is-scrolled');
13    } else {
14      header.classList.remove('is-scrolled');
15    }
16  }
17
18  window.addEventListener('scroll', updateHeader, { passive: true });
19  updateHeader();
20})();
21
22/* =========================================
23   Hamburger Menu
24   ========================================= */
25(function () {
26  const hamburger = document.querySelector('.hamburger');
27  const mobileMenu = document.querySelector('.mobile-menu');
28  if (!hamburger || !mobileMenu) return;
29
30  function toggleMenu(open) {
31    hamburger.setAttribute('aria-expanded', String(open));
32    hamburger.setAttribute('aria-label', open ? 'メニューを閉じる' : 'メニューを開く');
33    mobileMenu.setAttribute('aria-hidden', String(!open));
34    mobileMenu.classList.toggle('is-open', open);
35    document.body.style.overflow = open ? 'hidden' : '';
36  }
37
38  hamburger.addEventListener('click', function () {
39    const isOpen = hamburger.getAttribute('aria-expanded') === 'true';
40    toggleMenu(!isOpen);
41  });
42
43  // モバイルメニューのリンクをクリックしたら閉じる
44  mobileMenu.querySelectorAll('.mobile-nav-link').forEach(function (link) {
45    link.addEventListener('click', function () {
46      toggleMenu(false);
47    });
48  });
49
50  // ESCキーで閉じる
51  document.addEventListener('keydown', function (e) {
52    if (e.key === 'Escape' && hamburger.getAttribute('aria-expanded') === 'true') {
53      toggleMenu(false);
54      hamburger.focus();
55    }
56  });
57})();
58
59/* =========================================
60   IntersectionObserver: スクロールフェードイン
61   ========================================= */
62(function () {
63  const targets = document.querySelectorAll('.js-fade');
64  if (!targets.length) return;
65
66  const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
67
68  if (reduceMotion || !('IntersectionObserver' in window)) {
69    targets.forEach(function (el) {
70      el.classList.add('is-visible');
71    });
72    return;
73  }
74
75  const observer = new IntersectionObserver(
76    function (entries) {
77      entries.forEach(function (entry) {
78        if (entry.isIntersecting) {
79          entry.target.classList.add('is-visible');
80          observer.unobserve(entry.target);
81        }
82      });
83    },
84    {
85      rootMargin: '0px 0px -60px 0px',
86      threshold: 0.12,
87    }
88  );
89
90  // 初期表示時にすでにビューポート内にある要素は、スクロールを待たず即座に表示する
91  targets.forEach(function (el) {
92    const rect = el.getBoundingClientRect();
93    const alreadyVisible = rect.top < window.innerHeight && rect.bottom > 0;
94    if (alreadyVisible) {
95      el.classList.add('is-visible');
96    } else {
97      observer.observe(el);
98    }
99  });
100})();
101
102/* =========================================
103   スムーススクロール(アンカーリンク)
104   ========================================= */
105(function () {
106  document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
107    anchor.addEventListener('click', function (e) {
108      const href = anchor.getAttribute('href');
109      if (href === '#') return;
110
111      const target = document.querySelector(href);
112      if (!target) return;
113
114      e.preventDefault();
115
116      // モバイルメニューが開いている状態からの遷移は、閉じるのを待たず即座にジャンプする
117      var openMobileNav = document.querySelector('.mobile-nav.is-open');
118      var instant = !!openMobileNav;
119      if (openMobileNav) {
120        openMobileNav.classList.remove('is-open');
121        document.body.style.overflow = '';
122        var hamburgerEl = document.querySelector('.hamburger');
123        if (hamburgerEl) {
124          hamburgerEl.setAttribute('aria-expanded', 'false');
125          hamburgerEl.setAttribute('aria-label', 'メニューを開く');
126        }
127      }
128
129      const headerEl = document.querySelector('.site-header') || document.querySelector('.header');
130      const headerHeight = headerEl ? headerEl.offsetHeight : 0;
131      const targetTop = target.getBoundingClientRect().top + window.scrollY - headerHeight;
132
133      window.scrollTo({ top: targetTop, behavior: instant ? 'auto' : 'smooth' });
134      target.setAttribute('tabindex', '-1');
135      target.focus({ preventScroll: true });
136    });
137  });
138})();
139
140/* =========================================
141   Contact Form: バリデーション & 送信処理
142   ========================================= */
143(function () {
144  const form = document.getElementById('contact-form');
145  const successMsg = document.getElementById('contact-success');
146  const errorMsg = document.getElementById('contact-form-error');
147  if (!form || !successMsg) return;
148
149  function showError(input, message) {
150    clearError(input);
151    input.setAttribute('aria-invalid', 'true');
152    const errorEl = document.createElement('span');
153    errorEl.className = 'form-error';
154    errorEl.id = input.id + '-error';
155    errorEl.textContent = message;
156    errorEl.style.cssText = 'display:block;margin-top:4px;font-size:13px;color:#DC2626;';
157    input.setAttribute('aria-describedby', errorEl.id);
158    input.parentNode.appendChild(errorEl);
159  }
160
161  function clearError(input) {
162    input.removeAttribute('aria-invalid');
163    input.removeAttribute('aria-describedby');
164    const existing = input.parentNode.querySelector('.form-error');
165    if (existing) existing.remove();
166  }
167
168  function validateField(input) {
169    clearError(input);
170    if (input.required && !input.value.trim()) {
171      showError(input, 'この項目は必須です。');
172      return false;
173    }
174    if (input.type === 'email' && input.value.trim()) {
175      const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
176      if (!emailRe.test(input.value.trim())) {
177        showError(input, '正しいメールアドレスを入力してください。');
178        return false;
179      }
180    }
181    return true;
182  }
183
184  // リアルタイムバリデーション(フォーカスアウト時)
185  form.querySelectorAll('.form-input, .form-textarea').forEach(function (input) {
186    input.addEventListener('blur', function () {
187      validateField(input);
188    });
189  });
190
191  form.addEventListener('submit', function (e) {
192    e.preventDefault();
193
194    const fields = form.querySelectorAll('.form-input, .form-textarea');
195    let isValid = true;
196
197    fields.forEach(function (field) {
198      if (!validateField(field)) {
199        isValid = false;
200      }
201    });
202
203    if (!isValid) {
204      const firstError = form.querySelector('[aria-invalid="true"]');
205      if (firstError) firstError.focus();
206      return;
207    }
208
209    const submitBtn = form.querySelector('[type="submit"]');
210    const originalBtnText = submitBtn.textContent;
211    submitBtn.disabled = true;
212    submitBtn.textContent = '送信中…';
213    if (errorMsg) errorMsg.setAttribute('hidden', '');
214
215    function restoreForm(err) {
216      console.warn('contact form submit error:', err);
217      submitBtn.disabled = false;
218      submitBtn.textContent = originalBtnText;
219      if (errorMsg) {
220        errorMsg.removeAttribute('hidden');
221        errorMsg.scrollIntoView({ behavior: 'smooth', block: 'center' });
222      }
223    }
224
225    let payload;
226    try {
227      const el = form.elements;
228      payload = {
229        name: el.name.value.trim(),
230        company: el.company.value.trim(),
231        email: el.email.value.trim(),
232        category: el.category.value,
233        message: el.message.value.trim(),
234        _subject: '[Corporate Site] お問い合わせ',
235      };
236    } catch (err) {
237      restoreForm(err);
238      return;
239    }
240
241    fetch('https://formspree.io/f/xdawzban', {
242      method: 'POST',
243      headers: { 'Content-Type': 'application/json' },
244      body: JSON.stringify(payload),
245    })
246      .then(function (res) {
247        if (!res.ok) throw new Error('Formspree returned ' + res.status);
248        form.setAttribute('hidden', '');
249        successMsg.removeAttribute('hidden');
250        successMsg.scrollIntoView({ behavior: 'smooth', block: 'center' });
251      })
252      .catch(restoreForm);
253  });
254})();
255
256/* =========================================
257   Sub-page Mobile Nav (.mobile-nav)
258   ========================================= */
259(function () {
260  var hamburger = document.querySelector('.hamburger');
261  var mobileNav = document.querySelector('.mobile-nav');
262  var closeBtn = document.querySelector('.mobile-nav__close');
263  // Only run when .mobile-nav (not .mobile-menu) is present
264  if (!hamburger || !mobileNav || document.querySelector('.mobile-menu')) return;
265
266  function openNav() {
267    mobileNav.classList.add('is-open');
268    mobileNav.setAttribute('aria-hidden', 'false');
269    hamburger.setAttribute('aria-expanded', 'true');
270    hamburger.setAttribute('aria-label', 'メニューを閉じる');
271    document.body.style.overflow = 'hidden';
272  }
273
274  function closeNav() {
275    mobileNav.classList.remove('is-open');
276    mobileNav.setAttribute('aria-hidden', 'true');
277    hamburger.setAttribute('aria-expanded', 'false');
278    hamburger.setAttribute('aria-label', 'メニューを開く');
279    document.body.style.overflow = '';
280  }
281
282  hamburger.addEventListener('click', function () {
283    if (mobileNav.classList.contains('is-open')) {
284      closeNav();
285    } else {
286      openNav();
287    }
288  });
289
290  if (closeBtn) {
291    closeBtn.addEventListener('click', closeNav);
292  }
293
294  mobileNav.querySelectorAll('.mobile-nav__link').forEach(function (link) {
295    link.addEventListener('click', closeNav);
296  });
297
298  document.addEventListener('keydown', function (e) {
299    if (e.key === 'Escape') closeNav();
300  });
301})();
302
303/* =========================================
304   Reveal Animation (.reveal)
305   ========================================= */
306(function () {
307  var targets = document.querySelectorAll('.reveal');
308  if (!targets.length) return;
309
310  var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
311
312  if (reduceMotion || !('IntersectionObserver' in window)) {
313    targets.forEach(function (el) { el.classList.add('is-visible'); });
314    return;
315  }
316
317  var observer = new IntersectionObserver(
318    function (entries) {
319      entries.forEach(function (entry) {
320        if (entry.isIntersecting) {
321          entry.target.classList.add('is-visible');
322          observer.unobserve(entry.target);
323        }
324      });
325    },
326    { rootMargin: '0px 0px -60px 0px', threshold: 0.08 }
327  );
328
329  // 初期表示時にすでにビューポート内にある要素は、スクロールを待たず即座に表示する
330  targets.forEach(function (el) {
331    var rect = el.getBoundingClientRect();
332    var alreadyVisible = rect.top < window.innerHeight && rect.bottom > 0;
333    if (alreadyVisible) {
334      el.classList.add('is-visible');
335    } else {
336      observer.observe(el);
337    }
338  });
339})();
340
341/* =========================================
342   FAQ Accordion
343   ========================================= */
344(function () {
345  var items = document.querySelectorAll('.faq-item');
346  if (!items.length) return;
347
348  items.forEach(function (item) {
349    var btn = item.querySelector('.faq-question');
350    var answer = item.querySelector('.faq-answer');
351    if (!btn || !answer) return;
352
353    btn.addEventListener('click', function () {
354      var isOpen = item.classList.contains('is-open');
355      // Close all
356      items.forEach(function (i) {
357        i.classList.remove('is-open');
358        var a = i.querySelector('.faq-answer');
359        var b = i.querySelector('.faq-question');
360        if (a) a.hidden = true;
361        if (b) b.setAttribute('aria-expanded', 'false');
362      });
363      // Open clicked
364      if (!isOpen) {
365        item.classList.add('is-open');
366        answer.hidden = false;
367        btn.setAttribute('aria-expanded', 'true');
368      }
369    });
370  });
371})();
372
373/* =========================================
374   Whitepaper / Report Download Form
375   ========================================= */
376(function () {
377  var form = document.getElementById('wp-form');
378  var success = document.getElementById('wp-form-success');
379  if (!form || !success) return;
380
381  form.addEventListener('submit', function (e) {
382    e.preventDefault();
383
384    var name    = form.querySelector('[name="name"]').value.trim();
385    var company = form.querySelector('[name="company"]').value.trim();
386    var email   = form.querySelector('[name="email"]').value.trim();
387    var deptEl  = form.querySelector('[name="department"]');
388    var dept    = deptEl ? deptEl.value.trim() : '';
389
390    if (!name || !company || !email) {
391      var firstEmpty = form.querySelector('input:invalid');
392      if (firstEmpty) firstEmpty.focus();
393      return;
394    }
395
396    var submitBtn = form.querySelector('.wp-form__submit');
397    var originalBtnText = submitBtn.textContent;
398    submitBtn.disabled = true;
399    submitBtn.textContent = '送信中…';
400
401    fetch('https://medeco.io/api/mmp-download', {
402      method: 'POST',
403      headers: { 'Content-Type': 'application/json' },
404      body: JSON.stringify({ name: name, company: company, department: dept, email: email }),
405    }).then(function (res) {
406      if (!res.ok) throw new Error('API returned ' + res.status);
407      form.setAttribute('hidden', '');
408      success.removeAttribute('hidden');
409    }).catch(function (err) {
410      console.warn('mmp-download API error:', err);
411      submitBtn.disabled = false;
412      submitBtn.textContent = originalBtnText;
413      alert('送信に失敗しました。時間をおいて再度お試しください。');
414    });
415  });
416})();
417
418/* =========================================
419   Chat Widget
420   ========================================= */
421(function () {
422  var widget = document.getElementById('chatWidget');
423  var toggle = document.getElementById('chatToggle');
424  var panel = document.getElementById('chatPanel');
425  var form = document.getElementById('chatForm');
426  var successMsg = document.getElementById('chatSuccess');
427  var errorMsg = document.getElementById('chatError');
428  if (!widget || !toggle || !panel || !form || !successMsg) return;
429
430  var isOpen = false;
431
432  function getFocusable() {
433    return Array.prototype.slice.call(
434      panel.querySelectorAll('input, textarea, button, a[href]')
435    ).filter(function (el) { return !el.disabled && el.offsetParent !== null; });
436  }
437
438  function openPanel() {
439    isOpen = true;
440    panel.classList.add('is-open');
441    panel.setAttribute('aria-hidden', 'false');
442    toggle.setAttribute('aria-expanded', 'true');
443    toggle.setAttribute('aria-label', 'お問い合わせを閉じる');
444    var focusable = getFocusable();
445    if (focusable.length) focusable[0].focus();
446  }
447
448  function closePanel(returnFocus) {
449    isOpen = false;
450    panel.classList.remove('is-open');
451    panel.setAttribute('aria-hidden', 'true');
452    toggle.setAttribute('aria-expanded', 'false');
453    toggle.setAttribute('aria-label', 'お問い合わせを開く');
454    if (returnFocus !== false) toggle.focus();
455  }
456
457  toggle.addEventListener('click', function () {
458    if (isOpen) {
459      closePanel(true);
460    } else {
461      openPanel();
462    }
463  });
464
465  // Escapeで閉じる、Tabでフォーカストラップ
466  document.addEventListener('keydown', function (e) {
467    if (!isOpen) return;
468    if (e.key === 'Escape') {
469      closePanel(true);
470      return;
471    }
472    if (e.key === 'Tab') {
473      var focusable = getFocusable();
474      if (!focusable.length) return;
475      var first = focusable[0];
476      var last = focusable[focusable.length - 1];
477      if (e.shiftKey && document.activeElement === first) {
478        e.preventDefault();
479        last.focus();
480      } else if (!e.shiftKey && document.activeElement === last) {
481        e.preventDefault();
482        first.focus();
483      }
484    }
485  });
486
487  // パネル外クリックで閉じる
488  document.addEventListener('click', function (e) {
489    if (!isOpen) return;
490    if (widget.contains(e.target)) return;
491    closePanel(false);
492  });
493
494  // バリデーション(既存contact-formと同等のロジック)
495  function showFieldError(input, message) {
496    clearFieldError(input);
497    input.setAttribute('aria-invalid', 'true');
498    var errorEl = document.createElement('span');
499    errorEl.className = 'form-error';
500    errorEl.id = input.id + '-error';
501    errorEl.textContent = message;
502    errorEl.style.cssText = 'display:block;margin-top:4px;font-size:12px;color:#DC2626;';
503    input.setAttribute('aria-describedby', errorEl.id);
504    input.parentNode.appendChild(errorEl);
505  }
506
507  function clearFieldError(input) {
508    input.removeAttribute('aria-invalid');
509    input.removeAttribute('aria-describedby');
510    var existing = input.parentNode.querySelector('.form-error');
511    if (existing) existing.remove();
512  }
513
514  function validateChatField(input) {
515    clearFieldError(input);
516    if (input.required && !input.value.trim()) {
517      showFieldError(input, 'この項目は必須です。');
518      return false;
519    }
520    if (input.type === 'email' && input.value.trim()) {
521      var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
522      if (!emailRe.test(input.value.trim())) {
523        showFieldError(input, '正しいメールアドレスを入力してください。');
524        return false;
525      }
526    }
527    return true;
528  }
529
530  form.querySelectorAll('input, textarea').forEach(function (input) {
531    input.addEventListener('blur', function () { validateChatField(input); });
532  });
533
534  form.addEventListener('submit', function (e) {
535    e.preventDefault();
536
537    var fields = form.querySelectorAll('input:not([type="hidden"]), textarea');
538    var isValid = true;
539    fields.forEach(function (field) {
540      if (!validateChatField(field)) isValid = false;
541    });
542
543    if (!isValid) {
544      var firstError = form.querySelector('[aria-invalid="true"]');
545      if (firstError) firstError.focus();
546      return;
547    }
548
549    var submitBtn = form.querySelector('.chat-widget__submit');
550    var originalText = submitBtn.textContent;
551    submitBtn.disabled = true;
552    submitBtn.setAttribute('aria-busy', 'true');
553    submitBtn.textContent = '送信中…';
554    if (errorMsg) errorMsg.setAttribute('hidden', '');
555
556    var name = document.getElementById('chatName').value.trim();
557    var email = document.getElementById('chatEmail').value.trim();
558    var message = document.getElementById('chatMessage').value.trim();
559    var subject = form.querySelector('[name="_subject"]').value;
560
561    fetch('https://formspree.io/f/xdawzban', {
562      method: 'POST',
563      headers: { 'Content-Type': 'application/json' },
564      body: JSON.stringify({ name: name, email: email, message: message, _subject: subject }),
565    })
566      .then(function (res) {
567        if (!res.ok) throw new Error('Formspree returned ' + res.status);
568        form.setAttribute('hidden', '');
569        successMsg.removeAttribute('hidden');
570      })
571      .catch(function (err) {
572        console.warn('chat widget submit error:', err);
573        submitBtn.disabled = false;
574        submitBtn.removeAttribute('aria-busy');
575        submitBtn.textContent = originalText;
576        if (errorMsg) errorMsg.removeAttribute('hidden');
577      });
578  });
579})();
580
581/* =========================================
582   Voices Carousel(医療現場の声)
583   1件を5秒表示してから次のスライドへ横送り
584   ========================================= */
585(function () {
586  var carousel = document.querySelector('.voices-carousel');
587  if (!carousel) return;
588
589  var track = carousel.querySelector('.voices-track');
590  var slides = carousel.querySelectorAll('.voice-slide');
591  var dots = carousel.querySelectorAll('.voices-dot');
592  var interval = parseInt(carousel.getAttribute('data-interval'), 10) || 5000;
593  var index = 0;
594  var timer = null;
595
596  function goTo(i) {
597    index = (i + slides.length) % slides.length;
598    track.style.transform = 'translateX(-' + (index * 100) + '%)';
599    dots.forEach(function (dot, di) {
600      var active = di === index;
601      dot.classList.toggle('is-active', active);
602      dot.setAttribute('aria-selected', String(active));
603    });
604  }
605
606  function next() {
607    goTo(index + 1);
608  }
609
610  function start() {
611    stop();
612    timer = setInterval(next, interval);
613  }
614
615  function stop() {
616    if (timer) {
617      clearInterval(timer);
618      timer = null;
619    }
620  }
621
622  dots.forEach(function (dot, di) {
623    dot.addEventListener('click', function () {
624      goTo(di);
625      start();
626    });
627  });
628
629  carousel.addEventListener('mouseenter', stop);
630  carousel.addEventListener('mouseleave', start);
631  carousel.addEventListener('focusin', stop);
632  carousel.addEventListener('focusout', start);
633
634  goTo(0);
635
636  // ページ読み込み直後から自動再生を始めると、ユーザーがスクロールして
637  // このセクションに到達した時点で最初のコメント以外が表示されてしまうため、
638  // 実際にビューポートに入ってから自動再生を開始する
639  if (slides.length > 1 && 'IntersectionObserver' in window) {
640    var started = false;
641    var visibilityObserver = new IntersectionObserver(
642      function (entries) {
643        entries.forEach(function (entry) {
644          if (entry.isIntersecting && !started) {
645            started = true;
646            goTo(0);
647            start();
648            visibilityObserver.disconnect();
649          }
650        });
651      },
652      { threshold: 0.3 }
653    );
654    visibilityObserver.observe(carousel);
655  } else if (slides.length > 1) {
656    start();
657  }
658})();
659
660/* =========================================
661   Services 画面スライドショー(スマホモックアップ内の画面を
662   フェードイン・フェードアウトで自動切り替え)
663   ========================================= */
664(function () {
665  var frame = document.querySelector('.device-frame--shot');
666  if (!frame) return;
667
668  var slides = frame.querySelectorAll('.device-frame__slide');
669  if (slides.length < 2) return;
670
671  var index = 0;
672  slides.forEach(function (slide, i) {
673    if (slide.classList.contains('is-active')) index = i;
674  });
675
676  var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
677  if (prefersReducedMotion) return;
678
679  setInterval(function () {
680    slides[index].classList.remove('is-active');
681    index = (index + 1) % slides.length;
682    slides[index].classList.add('is-active');
683  }, 4000);
684})();
685
686/* =========================================
687   Medeco Pro LP: 診療科タブ
688   ========================================= */
689(function () {
690  var tabs = document.querySelectorAll('.mp-dept-tab');
691  if (!tabs.length) return;
692
693  var panels = document.querySelectorAll('.mp-dept-panel');
694
695  function activate(dept) {
696    tabs.forEach(function (tab) {
697      var active = tab.getAttribute('data-dept') === dept;
698      tab.classList.toggle('is-active', active);
699      tab.setAttribute('aria-selected', String(active));
700    });
701    panels.forEach(function (panel) {
702      panel.classList.toggle('is-active', panel.getAttribute('data-dept') === dept);
703    });
704  }
705
706  tabs.forEach(function (tab) {
707    tab.addEventListener('click', function () {
708      activate(tab.getAttribute('data-dept'));
709    });
710  });
711})();

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.