フロントエンド 2025.12.25

現場で本当に使える!JavaScript実用スニペット集

約117分で読めます

フロントエンド開発でよく使う機能を厳選。DOM操作、フォーム処理、アニメーションなど、コピペで使えるJavaScriptコードをクライアントワークの実例と共に紹介します。

はじめに

「また同じ処理を書いている…」「前にも作ったはずなのに、どこにあるか分からない…」

Web制作の現場では、こうした場面に何度も遭遇します。本スニペット集は、実際のクライアントワークで繰り返し使用してきた実用的なJavaScriptコードを体系的にまとめたものです。

このスニペット集の特徴

  • 実務で検証済み - 実案件で使用し、動作確認されたコードのみを収録
  • コピペで即使用 - 依存ライブラリなし、そのまま使える純粋なJavaScript
  • エラーに強い - null/undefined チェック、try-catch など防御的な実装
  • モダンな記法 - ES6+の構文を採用(必要に応じてトランスパイル)

対象読者

  • JavaScriptの基本は理解しているが、実務パターンを増やしたい方
  • jQueryから脱却し、Vanilla JSに移行したい方
  • チーム内で共通のユーティリティ関数を整備したい方
  • 毎回ググるのをやめて、手元にリファレンスを持ちたい方

使い方

  1. 必要なスニペットを見つけて、プロジェクトにコピー
  2. 用途に合わせて関数名やオプションをカスタマイズ
  3. 複数のスニペットを組み合わせて、より複雑な機能を構築
推奨ディレクトリ構成:
/js
  /utils       ... 汎用ユーティリティ(DOM操作、フォーマットなど)
  /components  ... UI部品(モーダル、タブなど)
  main.js      ... エントリーポイント

動作環境

  • モダンブラウザ(Chrome, Firefox, Safari, Edge 最新版)
  • IE11非対応(必要な場合はBabelでトランスパイル)

目次

  1. DOM操作
  2. フォーム処理
  3. イベント制御
  4. スクロール・アニメーション
  5. API・非同期処理
  6. URL・パラメータ操作
  7. ストレージ・Cookie
  8. 日付・時間
  9. 数値・文字列フォーマット
  10. 配列・オブジェクト操作
  11. 画像・メディア
  12. クリップボード
  13. デバイス・環境検知
  14. UI部品
  15. アクセシビリティ
  16. セキュリティ・サニタイズ
  17. 印刷
  18. ユーティリティ

1. DOM操作

要素の安全な取得

// 単一要素の取得(存在チェック付き)
function $(selector, context = document) {
  const element = context.querySelector(selector);
  if (!element) {
    console.warn(`Element not found: ${selector}`);
    return null;
  }
  return element;
}

// 複数要素の取得(配列として返す)
function $$(selector, context = document) {
  return [...context.querySelectorAll(selector)];
}

// 使用例
const header = $('#header');
const buttons = $$('.btn');

クラス操作

// 安全なクラストグル
function toggleClass(selector, className, condition = null) {
  const element = $(selector);
  if (!element) return false;
  
  if (condition !== null) {
    element.classList.toggle(className, condition);
  } else {
    element.classList.toggle(className);
  }
  return true;
}

// 複数要素に一括でクラス追加/削除
function setClassAll(selector, className, add = true) {
  $$(selector).forEach(el => {
    el.classList[add ? 'add' : 'remove'](className);
  });
}

// 使用例
toggleClass('.menu', 'is-open');
setClassAll('.tab', 'is-active', false);

要素の作成と挿入

// 要素を簡単に作成
function createElement(tag, attributes = {}, children = []) {
  const element = document.createElement(tag);
  
  Object.entries(attributes).forEach(([key, value]) => {
    if (key === 'className') {
      element.className = value;
    } else if (key === 'dataset') {
      Object.entries(value).forEach(([dataKey, dataValue]) => {
        element.dataset[dataKey] = dataValue;
      });
    } else if (key.startsWith('on') && typeof value === 'function') {
      element.addEventListener(key.slice(2).toLowerCase(), value);
    } else {
      element.setAttribute(key, value);
    }
  });
  
  children.forEach(child => {
    if (typeof child === 'string') {
      element.appendChild(document.createTextNode(child));
    } else {
      element.appendChild(child);
    }
  });
  
  return element;
}

// 使用例
const button = createElement('button', {
  className: 'btn btn-primary',
  dataset: { action: 'submit' },
  onClick: () => console.log('clicked')
}, ['送信']);

要素の表示/非表示

// 表示切り替え
function show(selector) {
  const el = $(selector);
  if (el) el.style.display = '';
}

function hide(selector) {
  const el = $(selector);
  if (el) el.style.display = 'none';
}

function toggle(selector, condition) {
  const el = $(selector);
  if (el) el.style.display = condition ? '' : 'none';
}

// フェードイン/フェードアウト
function fadeIn(selector, duration = 300) {
  const el = $(selector);
  if (!el) return;
  
  el.style.opacity = 0;
  el.style.display = '';
  el.style.transition = `opacity ${duration}ms`;
  
  requestAnimationFrame(() => {
    el.style.opacity = 1;
  });
}

function fadeOut(selector, duration = 300) {
  const el = $(selector);
  if (!el) return;
  
  el.style.transition = `opacity ${duration}ms`;
  el.style.opacity = 0;
  
  setTimeout(() => {
    el.style.display = 'none';
  }, duration);
}

親・兄弟要素の取得

// 条件に合う親要素を取得
function closest(element, selector) {
  return element.closest(selector);
}

// 兄弟要素を取得
function siblings(element) {
  return [...element.parentNode.children].filter(child => child !== element);
}

// 次/前の兄弟要素
function next(element, selector = null) {
  let sibling = element.nextElementSibling;
  if (!selector) return sibling;
  
  while (sibling) {
    if (sibling.matches(selector)) return sibling;
    sibling = sibling.nextElementSibling;
  }
  return null;
}

function prev(element, selector = null) {
  let sibling = element.previousElementSibling;
  if (!selector) return sibling;
  
  while (sibling) {
    if (sibling.matches(selector)) return sibling;
    sibling = sibling.previousElementSibling;
  }
  return null;
}

2. フォーム処理

フォームデータの取得

// フォームデータをオブジェクトとして取得
function getFormData(formSelector) {
  const form = $(formSelector);
  if (!form) return null;
  
  const formData = new FormData(form);
  const data = {};
  
  for (let [key, value] of formData.entries()) {
    // 同じ名前のフィールドが複数ある場合は配列に
    if (data[key]) {
      if (!Array.isArray(data[key])) {
        data[key] = [data[key]];
      }
      data[key].push(value);
    } else {
      data[key] = typeof value === 'string' ? value.trim() : value;
    }
  }
  
  return data;
}

// 使用例
const data = getFormData('#contact-form');
// { name: '山田太郎', email: '[email protected]', ... }

バリデーション

// バリデーションルール
const validators = {
  required: (value) => value.length > 0,
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  phone: (value) => /^[0-9\-]+$/.test(value),
  postalCode: (value) => /^\d{3}-?\d{4}$/.test(value),
  url: (value) => {
    try {
      new URL(value);
      return true;
    } catch {
      return false;
    }
  },
  minLength: (min) => (value) => value.length >= min,
  maxLength: (max) => (value) => value.length <= max,
  pattern: (regex) => (value) => regex.test(value),
  match: (fieldName) => (value, formData) => value === formData[fieldName],
};

// フィールドのバリデーション
function validateField(field, rules, formData = {}) {
  const value = field.value.trim();
  const errors = [];
  
  for (const [ruleName, ruleConfig] of Object.entries(rules)) {
    const { message, ...options } = typeof ruleConfig === 'string' 
      ? { message: ruleConfig } 
      : ruleConfig;
    
    let validator = validators[ruleName];
    let isValid = true;
    
    if (typeof validator === 'function') {
      // パラメータ付きバリデータ
      if (options.value !== undefined) {
        validator = validator(options.value);
      }
      isValid = !value && ruleName !== 'required' ? true : validator(value, formData);
    }
    
    if (!isValid) {
      errors.push(message || `${ruleName}の検証に失敗しました`);
    }
  }
  
  return errors;
}

// フォーム全体のバリデーション
function validateForm(formSelector, rulesConfig) {
  const form = $(formSelector);
  if (!form) return { isValid: false, errors: {} };
  
  const formData = getFormData(formSelector);
  const errors = {};
  let isValid = true;
  
  for (const [fieldName, rules] of Object.entries(rulesConfig)) {
    const field = form.querySelector(`[name="${fieldName}"]`);
    if (!field) continue;
    
    const fieldErrors = validateField(field, rules, formData);
    if (fieldErrors.length > 0) {
      errors[fieldName] = fieldErrors;
      isValid = false;
    }
  }
  
  return { isValid, errors };
}

// 使用例
const { isValid, errors } = validateForm('#signup-form', {
  email: {
    required: { message: 'メールアドレスは必須です' },
    email: { message: '正しいメールアドレスを入力してください' }
  },
  password: {
    required: { message: 'パスワードは必須です' },
    minLength: { value: 8, message: 'パスワードは8文字以上で入力してください' }
  },
  password_confirm: {
    match: { value: 'password', message: 'パスワードが一致しません' }
  }
});

リアルタイムバリデーション

// リアルタイムバリデーションのセットアップ
function setupRealtimeValidation(formSelector, rulesConfig, options = {}) {
  const form = $(formSelector);
  if (!form) return;
  
  const {
    errorClass = 'is-error',
    errorMessageClass = 'error-message',
    validateOn = 'blur' // 'blur', 'input', 'change'
  } = options;
  
  function showError(field, messages) {
    field.classList.add(errorClass);
    let errorEl = field.parentNode.querySelector(`.${errorMessageClass}`);
    
    if (!errorEl) {
      errorEl = document.createElement('span');
      errorEl.className = errorMessageClass;
      field.parentNode.appendChild(errorEl);
    }
    
    errorEl.textContent = messages[0];
    errorEl.style.display = 'block';
  }
  
  function clearError(field) {
    field.classList.remove(errorClass);
    const errorEl = field.parentNode.querySelector(`.${errorMessageClass}`);
    if (errorEl) {
      errorEl.style.display = 'none';
    }
  }
  
  for (const [fieldName, rules] of Object.entries(rulesConfig)) {
    const field = form.querySelector(`[name="${fieldName}"]`);
    if (!field) continue;
    
    field.addEventListener(validateOn, () => {
      const formData = getFormData(formSelector);
      const errors = validateField(field, rules, formData);
      
      if (errors.length > 0) {
        showError(field, errors);
      } else {
        clearError(field);
      }
    });
  }
}

フォームのリセットと初期化

// フォームをリセット
function resetForm(formSelector) {
  const form = $(formSelector);
  if (!form) return;
  
  form.reset();
  
  // エラー表示もクリア
  $$('.is-error', form).forEach(el => el.classList.remove('is-error'));
  $$('.error-message', form).forEach(el => el.style.display = 'none');
}

// フォームに値をセット
function setFormValues(formSelector, values) {
  const form = $(formSelector);
  if (!form) return;
  
  for (const [name, value] of Object.entries(values)) {
    const field = form.querySelector(`[name="${name}"]`);
    if (!field) continue;
    
    if (field.type === 'checkbox') {
      field.checked = Boolean(value);
    } else if (field.type === 'radio') {
      const radio = form.querySelector(`[name="${name}"][value="${value}"]`);
      if (radio) radio.checked = true;
    } else if (field.tagName === 'SELECT') {
      field.value = value;
    } else {
      field.value = value;
    }
  }
}

3. イベント制御

デバウンス

// 連続した呼び出しを間引く(最後の呼び出しのみ実行)
function debounce(func, wait, immediate = false) {
  let timeout;
  
  return function executedFunction(...args) {
    const later = () => {
      timeout = null;
      if (!immediate) func.apply(this, args);
    };
    
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    
    if (callNow) func.apply(this, args);
  };
}

// 使用例:検索入力
const searchInput = $('#search');
if (searchInput) {
  const debouncedSearch = debounce((query) => {
    console.log('Searching:', query);
  }, 300);
  
  searchInput.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
  });
}

スロットル

// 実行頻度を制限(一定間隔で実行)
function throttle(func, limit) {
  let inThrottle;
  let lastResult;
  
  return function(...args) {
    if (!inThrottle) {
      lastResult = func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
    return lastResult;
  };
}

// 使用例:スクロールイベント
const throttledScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 100);

window.addEventListener('scroll', throttledScroll);

一度だけ実行

// 一度だけ実行される関数
function once(func) {
  let called = false;
  let result;
  
  return function(...args) {
    if (!called) {
      called = true;
      result = func.apply(this, args);
    }
    return result;
  };
}

// イベントリスナーを一度だけ実行
function addEventListenerOnce(element, event, handler) {
  element.addEventListener(event, handler, { once: true });
}

// 使用例
const initialize = once(() => {
  console.log('初期化処理(一度だけ実行)');
});

外側クリック検知

// 要素の外側クリックを検知
function onClickOutside(element, callback) {
  const handler = (event) => {
    if (!element.contains(event.target)) {
      callback(event);
    }
  };
  
  document.addEventListener('click', handler);
  
  // クリーンアップ関数を返す
  return () => document.removeEventListener('click', handler);
}

// 使用例:モーダルの外側クリックで閉じる
const modal = $('#modal');
const cleanup = onClickOutside(modal, () => {
  modal.classList.remove('is-open');
});

// 不要になったら cleanup() を呼ぶ

キーボードショートカット

// キーボードショートカットの登録
const shortcuts = new Map();

function registerShortcut(keys, callback, options = {}) {
  const { preventDefault = true, target = document } = options;
  
  const normalizedKeys = keys.toLowerCase().split('+').sort().join('+');
  
  if (!shortcuts.has(target)) {
    shortcuts.set(target, new Map());
    
    target.addEventListener('keydown', (e) => {
      const pressed = [];
      if (e.ctrlKey || e.metaKey) pressed.push('ctrl');
      if (e.shiftKey) pressed.push('shift');
      if (e.altKey) pressed.push('alt');
      pressed.push(e.key.toLowerCase());
      
      const combo = pressed.sort().join('+');
      const targetShortcuts = shortcuts.get(target);
      
      if (targetShortcuts.has(combo)) {
        if (preventDefault) e.preventDefault();
        targetShortcuts.get(combo)(e);
      }
    });
  }
  
  shortcuts.get(target).set(normalizedKeys, callback);
}

// 使用例
registerShortcut('ctrl+s', () => {
  console.log('保存');
});

registerShortcut('ctrl+shift+p', () => {
  console.log('コマンドパレット');
});

カスタムイベント

// カスタムイベントの発火
function emit(eventName, detail = {}, target = document) {
  const event = new CustomEvent(eventName, {
    detail,
    bubbles: true,
    cancelable: true
  });
  target.dispatchEvent(event);
}

// カスタムイベントの購読
function on(eventName, callback, target = document) {
  target.addEventListener(eventName, (e) => callback(e.detail, e));
  
  // クリーンアップ関数を返す
  return () => target.removeEventListener(eventName, callback);
}

// 使用例
on('cart:updated', (detail) => {
  console.log('カート更新:', detail);
});

emit('cart:updated', { itemCount: 5, total: 12000 });

イベント委譲

// イベント委譲(動的要素にも対応)
function delegate(parentSelector, eventType, childSelector, handler) {
  const parent = $(parentSelector);
  if (!parent) return;
  
  parent.addEventListener(eventType, (e) => {
    const target = e.target.closest(childSelector);
    if (target && parent.contains(target)) {
      handler.call(target, e);
    }
  });
}

// 使用例:動的に追加されるリストアイテムにも対応
delegate('#todo-list', 'click', '.todo-item', function(e) {
  console.log('クリックされたアイテム:', this.textContent);
});

4. スクロール・アニメーション

スムーススクロール

// スムーススクロール
function smoothScrollTo(target, options = {}) {
  const {
    offset = 0,
    duration = 800,
    easing = 'easeInOutQuad'
  } = options;
  
  const element = typeof target === 'string' ? $(target) : target;
  if (!element) return;
  
  const targetPosition = element.getBoundingClientRect().top + window.pageYOffset - offset;
  const startPosition = window.pageYOffset;
  const distance = targetPosition - startPosition;
  let startTime = null;
  
  const easingFunctions = {
    linear: t => t,
    easeInQuad: t => t * t,
    easeOutQuad: t => t * (2 - t),
    easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
    easeInCubic: t => t * t * t,
    easeOutCubic: t => (--t) * t * t + 1,
    easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  };
  
  function animation(currentTime) {
    if (startTime === null) startTime = currentTime;
    const timeElapsed = currentTime - startTime;
    const progress = Math.min(timeElapsed / duration, 1);
    const easedProgress = easingFunctions[easing](progress);
    
    window.scrollTo(0, startPosition + distance * easedProgress);
    
    if (timeElapsed < duration) {
      requestAnimationFrame(animation);
    }
  }
  
  requestAnimationFrame(animation);
}

// アンカーリンクにスムーススクロールを適用
function setupSmoothScroll(offset = 0) {
  $$('a[href^="#"]').forEach(link => {
    link.addEventListener('click', (e) => {
      const targetId = link.getAttribute('href');
      if (targetId === '#') return;
      
      e.preventDefault();
      smoothScrollTo(targetId, { offset });
      
      // URLのハッシュを更新
      history.pushState(null, '', targetId);
    });
  });
}

スクロール位置の取得・監視

// スクロール位置の取得
function getScrollPosition() {
  return {
    x: window.pageXOffset || document.documentElement.scrollLeft,
    y: window.pageYOffset || document.documentElement.scrollTop
  };
}

// スクロール進捗率
function getScrollProgress() {
  const scrollTop = window.pageYOffset;
  const docHeight = document.documentElement.scrollHeight - window.innerHeight;
  return docHeight > 0 ? scrollTop / docHeight : 0;
}

// スクロール方向の検知
function createScrollDirectionDetector() {
  let lastScrollY = window.pageYOffset;
  
  return function() {
    const currentScrollY = window.pageYOffset;
    const direction = currentScrollY > lastScrollY ? 'down' : 'up';
    lastScrollY = currentScrollY;
    return direction;
  };
}

// 使用例
const getScrollDirection = createScrollDirectionDetector();
window.addEventListener('scroll', throttle(() => {
  console.log('Direction:', getScrollDirection());
}, 100));

スクロールトリガーアニメーション

// IntersectionObserverによるスクロールアニメーション
function setupScrollAnimation(selector, options = {}) {
  const {
    threshold = 0.1,
    rootMargin = '0px 0px -50px 0px',
    animationClass = 'is-visible',
    once = true
  } = options;
  
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add(animationClass);
        if (once) {
          observer.unobserve(entry.target);
        }
      } else if (!once) {
        entry.target.classList.remove(animationClass);
      }
    });
  }, { threshold, rootMargin });
  
  $$(selector).forEach(el => observer.observe(el));
  
  return observer;
}

// 使用例
setupScrollAnimation('.fade-in', {
  animationClass: 'is-visible',
  once: true
});

// CSS
// .fade-in { opacity: 0; transform: translateY(20px); transition: all 0.6s; }
// .fade-in.is-visible { opacity: 1; transform: translateY(0); }

ヘッダーの固定・変化

// スクロールでヘッダーを変化させる
function setupStickyHeader(headerSelector, options = {}) {
  const header = $(headerSelector);
  if (!header) return;
  
  const {
    scrolledClass = 'is-scrolled',
    hiddenClass = 'is-hidden',
    threshold = 100,
    hideOnScrollDown = false
  } = options;
  
  let lastScrollY = 0;
  
  const handler = throttle(() => {
    const currentScrollY = window.pageYOffset;
    
    // スクロール位置に応じてクラスを付与
    header.classList.toggle(scrolledClass, currentScrollY > threshold);
    
    // 下スクロールで隠す
    if (hideOnScrollDown) {
      if (currentScrollY > lastScrollY && currentScrollY > threshold) {
        header.classList.add(hiddenClass);
      } else {
        header.classList.remove(hiddenClass);
      }
    }
    
    lastScrollY = currentScrollY;
  }, 100);
  
  window.addEventListener('scroll', handler);
}

トップへ戻るボタン

// トップへ戻るボタン
function setupBackToTop(buttonSelector, options = {}) {
  const button = $(buttonSelector);
  if (!button) return;
  
  const {
    showAfter = 300,
    visibleClass = 'is-visible'
  } = options;
  
  // 表示/非表示の制御
  window.addEventListener('scroll', throttle(() => {
    button.classList.toggle(visibleClass, window.pageYOffset > showAfter);
  }, 100));
  
  // クリックでトップへ
  button.addEventListener('click', () => {
    smoothScrollTo(document.body);
  });
}

スクロールロック

// スクロールロック(モーダル表示時など)
const scrollLock = {
  scrollPosition: 0,
  
  enable() {
    this.scrollPosition = window.pageYOffset;
    document.body.style.overflow = 'hidden';
    document.body.style.position = 'fixed';
    document.body.style.top = `-${this.scrollPosition}px`;
    document.body.style.width = '100%';
  },
  
  disable() {
    document.body.style.overflow = '';
    document.body.style.position = '';
    document.body.style.top = '';
    document.body.style.width = '';
    window.scrollTo(0, this.scrollPosition);
  }
};

// 使用例
function openModal() {
  scrollLock.enable();
  $('#modal').classList.add('is-open');
}

function closeModal() {
  scrollLock.disable();
  $('#modal').classList.remove('is-open');
}

カウントアップアニメーション

// 数値のカウントアップアニメーション
function countUp(element, options = {}) {
  const el = typeof element === 'string' ? $(element) : element;
  if (!el) return;
  
  const {
    start = 0,
    end = parseInt(el.textContent) || 0,
    duration = 2000,
    separator = ',',
    decimal = 0,
    prefix = '',
    suffix = ''
  } = options;
  
  let startTime = null;
  
  function animation(currentTime) {
    if (startTime === null) startTime = currentTime;
    const progress = Math.min((currentTime - startTime) / duration, 1);
    const easeProgress = 1 - Math.pow(1 - progress, 3); // easeOutCubic
    const current = start + (end - start) * easeProgress;
    
    el.textContent = prefix + formatNumber(current, decimal, separator) + suffix;
    
    if (progress < 1) {
      requestAnimationFrame(animation);
    }
  }
  
  requestAnimationFrame(animation);
}

function formatNumber(num, decimal, separator) {
  return num.toFixed(decimal).replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}

// スクロールでカウントアップを開始
function setupCountUpOnScroll(selector, options = {}) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        countUp(entry.target, options);
        observer.unobserve(entry.target);
      }
    });
  }, { threshold: 0.5 });
  
  $$(selector).forEach(el => observer.observe(el));
}

5. API・非同期処理

Fetchラッパー

// 高機能なfetchラッパー
async function fetchAPI(url, options = {}) {
  const {
    method = 'GET',
    headers = {},
    body = null,
    timeout = 10000,
    retries = 0,
    retryDelay = 1000,
    onProgress = null
  } = options;
  
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  const defaultHeaders = {
    'Content-Type': 'application/json'
  };
  
  const config = {
    method,
    headers: { ...defaultHeaders, ...headers },
    signal: controller.signal
  };
  
  if (body && method !== 'GET') {
    config.body = JSON.stringify(body);
  }
  
  async function attemptFetch(retriesLeft) {
    try {
      const response = await fetch(url, config);
      clearTimeout(timeoutId);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data = await response.json();
      return { data, response };
    } catch (error) {
      if (retriesLeft > 0 && error.name !== 'AbortError') {
        await new Promise(resolve => setTimeout(resolve, retryDelay));
        return attemptFetch(retriesLeft - 1);
      }
      throw error;
    }
  }
  
  return attemptFetch(retries);
}

// 使用例
try {
  const { data } = await fetchAPI('/api/users', {
    timeout: 5000,
    retries: 3
  });
  console.log(data);
} catch (error) {
  console.error('API Error:', error);
}

並列・直列リクエスト

// 並列リクエスト(すべて完了を待つ)
async function fetchAll(urls) {
  const results = await Promise.all(
    urls.map(url => fetchAPI(url).catch(err => ({ error: err })))
  );
  return results;
}

// 並列リクエスト(最初に成功したものを返す)
async function fetchRace(urls) {
  return Promise.race(urls.map(url => fetchAPI(url)));
}

// 直列リクエスト
async function fetchSequential(urls) {
  const results = [];
  for (const url of urls) {
    try {
      const result = await fetchAPI(url);
      results.push(result);
    } catch (error) {
      results.push({ error });
    }
  }
  return results;
}

// 並列リクエスト(同時実行数を制限)
async function fetchWithConcurrency(urls, maxConcurrency = 3) {
  const results = [];
  const executing = [];
  
  for (const url of urls) {
    const promise = fetchAPI(url).then(result => {
      executing.splice(executing.indexOf(promise), 1);
      return result;
    });
    
    results.push(promise);
    executing.push(promise);
    
    if (executing.length >= maxConcurrency) {
      await Promise.race(executing);
    }
  }
  
  return Promise.all(results);
}

ローディング状態管理

// ローディング状態を管理
function createLoadingState(element) {
  const el = typeof element === 'string' ? $(element) : element;
  let originalContent = '';
  
  return {
    start(loadingText = '読み込み中...') {
      if (el) {
        originalContent = el.innerHTML;
        el.innerHTML = loadingText;
        el.disabled = true;
        el.classList.add('is-loading');
      }
    },
    
    stop() {
      if (el) {
        el.innerHTML = originalContent;
        el.disabled = false;
        el.classList.remove('is-loading');
      }
    }
  };
}

// 使用例
const submitButton = createLoadingState('#submit-btn');

async function handleSubmit() {
  submitButton.start();
  try {
    await fetchAPI('/api/submit', { method: 'POST', body: formData });
  } finally {
    submitButton.stop();
  }
}

async/awaitエラーハンドリング

// try-catchを簡潔に
async function tryCatch(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error, null];
  }
}

// 使用例
const [error, data] = await tryCatch(fetchAPI('/api/users'));

if (error) {
  console.error('Error:', error);
} else {
  console.log('Data:', data);
}

6. URL・パラメータ操作

URLパラメータの操作

// URLパラメータを取得
function getQueryParams(url = window.location.href) {
  const params = new URL(url).searchParams;
  const result = {};
  
  for (const [key, value] of params) {
    result[key] = value;
  }
  
  return result;
}

// 特定のパラメータを取得
function getQueryParam(name, url = window.location.href) {
  const params = new URL(url).searchParams;
  return params.get(name);
}

// パラメータを追加・更新
function setQueryParam(name, value, url = window.location.href) {
  const urlObj = new URL(url);
  urlObj.searchParams.set(name, value);
  return urlObj.toString();
}

// パラメータを削除
function removeQueryParam(name, url = window.location.href) {
  const urlObj = new URL(url);
  urlObj.searchParams.delete(name);
  return urlObj.toString();
}

// 複数のパラメータを一括設定
function setQueryParams(params, url = window.location.href) {
  const urlObj = new URL(url);
  
  for (const [key, value] of Object.entries(params)) {
    if (value === null || value === undefined) {
      urlObj.searchParams.delete(key);
    } else {
      urlObj.searchParams.set(key, value);
    }
  }
  
  return urlObj.toString();
}

// 使用例
const page = getQueryParam('page'); // '2'
const newUrl = setQueryParams({ page: 3, sort: 'date' });
history.pushState(null, '', newUrl);

URLの解析

// URLを解析
function parseURL(url) {
  const urlObj = new URL(url, window.location.origin);
  
  return {
    protocol: urlObj.protocol,
    host: urlObj.host,
    hostname: urlObj.hostname,
    port: urlObj.port,
    pathname: urlObj.pathname,
    search: urlObj.search,
    hash: urlObj.hash,
    params: getQueryParams(url),
    origin: urlObj.origin
  };
}

// パスからセグメントを取得
function getPathSegments(url = window.location.href) {
  const { pathname } = new URL(url, window.location.origin);
  return pathname.split('/').filter(Boolean);
}

// 使用例
const segments = getPathSegments('/products/category/shoes');
// ['products', 'category', 'shoes']

7. ストレージ・Cookie

localStorage/sessionStorageラッパー

// ストレージラッパー
function createStorage(storage) {
  return {
    get(key, defaultValue = null) {
      try {
        const item = storage.getItem(key);
        return item ? JSON.parse(item) : defaultValue;
      } catch {
        return defaultValue;
      }
    },
    
    set(key, value, expiresIn = null) {
      try {
        const item = {
          value,
          timestamp: Date.now(),
          expiresAt: expiresIn ? Date.now() + expiresIn : null
        };
        storage.setItem(key, JSON.stringify(item));
        return true;
      } catch {
        return false;
      }
    },
    
    remove(key) {
      storage.removeItem(key);
    },
    
    clear() {
      storage.clear();
    },
    
    // 期限切れチェック付きで取得
    getWithExpiry(key, defaultValue = null) {
      const item = this.get(key);
      if (!item) return defaultValue;
      
      if (item.expiresAt && Date.now() > item.expiresAt) {
        this.remove(key);
        return defaultValue;
      }
      
      return item.value;
    }
  };
}

const local = createStorage(localStorage);
const session = createStorage(sessionStorage);

// 使用例
local.set('user', { name: '山田' }, 24 * 60 * 60 * 1000); // 24時間
const user = local.getWithExpiry('user');

Cookie操作

// Cookie操作
const cookies = {
  get(name) {
    const matches = document.cookie.match(
      new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)')
    );
    return matches ? decodeURIComponent(matches[1]) : null;
  },
  
  set(name, value, options = {}) {
    const {
      expires = null,
      path = '/',
      domain = null,
      secure = false,
      sameSite = 'Lax'
    } = options;
    
    let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
    
    if (expires) {
      if (typeof expires === 'number') {
        const date = new Date();
        date.setTime(date.getTime() + expires * 1000);
        cookieString += `; expires=${date.toUTCString()}`;
      } else if (expires instanceof Date) {
        cookieString += `; expires=${expires.toUTCString()}`;
      }
    }
    
    cookieString += `; path=${path}`;
    if (domain) cookieString += `; domain=${domain}`;
    if (secure) cookieString += '; secure';
    cookieString += `; SameSite=${sameSite}`;
    
    document.cookie = cookieString;
  },
  
  remove(name, path = '/') {
    this.set(name, '', { expires: -1, path });
  },
  
  getAll() {
    const result = {};
    document.cookie.split(';').forEach(cookie => {
      const [name, value] = cookie.trim().split('=');
      if (name) result[decodeURIComponent(name)] = decodeURIComponent(value || '');
    });
    return result;
  }
};

// 使用例
cookies.set('theme', 'dark', { expires: 365 * 24 * 60 * 60 }); // 1年
const theme = cookies.get('theme');

8. 日付・時間

日付フォーマット

// 日付フォーマット
function formatDate(date, format = 'YYYY-MM-DD') {
  const d = date instanceof Date ? date : new Date(date);
  
  if (isNaN(d.getTime())) {
    return 'Invalid Date';
  }
  
  const pad = (num, len = 2) => String(num).padStart(len, '0');
  
  const tokens = {
    YYYY: d.getFullYear(),
    YY: String(d.getFullYear()).slice(-2),
    MM: pad(d.getMonth() + 1),
    M: d.getMonth() + 1,
    DD: pad(d.getDate()),
    D: d.getDate(),
    HH: pad(d.getHours()),
    H: d.getHours(),
    mm: pad(d.getMinutes()),
    m: d.getMinutes(),
    ss: pad(d.getSeconds()),
    s: d.getSeconds(),
    SSS: pad(d.getMilliseconds(), 3)
  };
  
  return format.replace(/YYYY|YY|MM|M|DD|D|HH|H|mm|m|ss|s|SSS/g, match => tokens[match]);
}

// 使用例
formatDate(new Date(), 'YYYY年MM月DD日 HH:mm');
// '2024年01月15日 14:30'

相対時間

// 相対時間表示
function timeAgo(date, locale = 'ja') {
  const d = date instanceof Date ? date : new Date(date);
  const now = new Date();
  const diff = now - d;
  const seconds = Math.floor(diff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);
  const weeks = Math.floor(days / 7);
  const months = Math.floor(days / 30);
  const years = Math.floor(days / 365);
  
  const labels = {
    ja: {
      now: 'たった今',
      seconds: (n) => `${n}秒前`,
      minutes: (n) => `${n}分前`,
      hours: (n) => `${n}時間前`,
      days: (n) => `${n}日前`,
      weeks: (n) => `${n}週間前`,
      months: (n) => `${n}ヶ月前`,
      years: (n) => `${n}年前`
    },
    en: {
      now: 'just now',
      seconds: (n) => `${n} seconds ago`,
      minutes: (n) => `${n} minutes ago`,
      hours: (n) => `${n} hours ago`,
      days: (n) => `${n} days ago`,
      weeks: (n) => `${n} weeks ago`,
      months: (n) => `${n} months ago`,
      years: (n) => `${n} years ago`
    }
  };
  
  const l = labels[locale] || labels.ja;
  
  if (seconds < 10) return l.now;
  if (seconds < 60) return l.seconds(seconds);
  if (minutes < 60) return l.minutes(minutes);
  if (hours < 24) return l.hours(hours);
  if (days < 7) return l.days(days);
  if (weeks < 4) return l.weeks(weeks);
  if (months < 12) return l.months(months);
  return l.years(years);
}

// 使用例
timeAgo(new Date('2024-01-10')); // '5日前'

日付計算

// 日付の加減算
function addDays(date, days) {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

function addMonths(date, months) {
  const result = new Date(date);
  result.setMonth(result.getMonth() + months);
  return result;
}

function addYears(date, years) {
  const result = new Date(date);
  result.setFullYear(result.getFullYear() + years);
  return result;
}

// 日付の差分
function diffDays(date1, date2) {
  const d1 = new Date(date1);
  const d2 = new Date(date2);
  const diff = Math.abs(d2 - d1);
  return Math.floor(diff / (1000 * 60 * 60 * 24));
}

// 期間内かどうか
function isWithinDays(date, days) {
  const d = new Date(date);
  const now = new Date();
  const diff = now - d;
  return diff >= 0 && diff <= days * 24 * 60 * 60 * 1000;
}

// 営業日判定(土日を除く)
function isBusinessDay(date) {
  const d = new Date(date);
  const day = d.getDay();
  return day !== 0 && day !== 6;
}

// 次の営業日を取得
function getNextBusinessDay(date) {
  let d = new Date(date);
  do {
    d = addDays(d, 1);
  } while (!isBusinessDay(d));
  return d;
}

日本の祝日判定

// 日本の祝日判定(簡易版)
function isJapaneseHoliday(date) {
  const d = new Date(date);
  const year = d.getFullYear();
  const month = d.getMonth() + 1;
  const day = d.getDate();
  
  // 固定祝日
  const fixedHolidays = {
    '1-1': '元日',
    '2-11': '建国記念の日',
    '2-23': '天皇誕生日',
    '4-29': '昭和の日',
    '5-3': '憲法記念日',
    '5-4': 'みどりの日',
    '5-5': 'こどもの日',
    '8-11': '山の日',
    '11-3': '文化の日',
    '11-23': '勤労感謝の日'
  };
  
  const key = `${month}-${day}`;
  if (fixedHolidays[key]) {
    return fixedHolidays[key];
  }
  
  // ハッピーマンデー
  const weekOfMonth = Math.ceil(day / 7);
  const dayOfWeek = d.getDay();
  
  if (dayOfWeek === 1) { // 月曜日
    if (month === 1 && weekOfMonth === 2) return '成人の日';
    if (month === 7 && weekOfMonth === 3) return '海の日';
    if (month === 9 && weekOfMonth === 3) return '敬老の日';
    if (month === 10 && weekOfMonth === 2) return 'スポーツの日';
  }
  
  return null;
}

9. 数値・文字列フォーマット

数値フォーマット

// カンマ区切り
function formatNumberWithCommas(num) {
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// 通貨フォーマット
function formatCurrency(num, currency = 'JPY', locale = 'ja-JP') {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(num);
}

// パーセント表示
function formatPercent(num, decimals = 0) {
  return (num * 100).toFixed(decimals) + '%';
}

// ファイルサイズ表示
function formatFileSize(bytes, decimals = 1) {
  if (bytes === 0) return '0 Bytes';
  
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  
  return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}

// 範囲内に収める
function clamp(num, min, max) {
  return Math.min(Math.max(num, min), max);
}

// 使用例
formatNumberWithCommas(1234567); // '1,234,567'
formatCurrency(1980);           // '¥1,980'
formatFileSize(1536000);        // '1.5 MB'

文字列操作

// 文字数制限(省略表示)
function truncate(str, maxLength, suffix = '...') {
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - suffix.length) + suffix;
}

// 文字数カウント(サロゲートペア対応)
function countCharacters(str) {
  return [...str].length;
}

// キャメルケース → ケバブケース
function toKebabCase(str) {
  return str
    .replace(/([a-z])([A-Z])/g, '$1-$2')
    .replace(/[\s_]+/g, '-')
    .toLowerCase();
}

// ケバブケース → キャメルケース
function toCamelCase(str) {
  return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}

// スネークケース → キャメルケース
function snakeToCamel(str) {
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}

// 全角→半角変換
function toHalfWidth(str) {
  return str.replace(/[A-Za-z0-9]/g, (s) => {
    return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
  }).replace(/ /g, ' ');
}

// 半角→全角変換
function toFullWidth(str) {
  return str.replace(/[A-Za-z0-9]/g, (s) => {
    return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
  }).replace(/ /g, ' ');
}

// ひらがな→カタカナ変換
function toKatakana(str) {
  return str.replace(/[\u3041-\u3096]/g, (ch) => {
    return String.fromCharCode(ch.charCodeAt(0) + 0x60);
  });
}

// カタカナ→ひらがな変換
function toHiragana(str) {
  return str.replace(/[\u30A1-\u30F6]/g, (ch) => {
    return String.fromCharCode(ch.charCodeAt(0) - 0x60);
  });
}

電話番号・郵便番号フォーマット

// 電話番号フォーマット
function formatPhoneNumber(value) {
  const digits = value.replace(/\D/g, '');
  
  // 携帯電話(11桁)
  if (digits.length === 11 && digits.startsWith('0')) {
    return digits.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
  }
  
  // 固定電話(10桁)
  if (digits.length === 10 && digits.startsWith('0')) {
    // 市外局番の桁数に応じて分割
    if (digits.startsWith('03') || digits.startsWith('06')) {
      return digits.replace(/(\d{2})(\d{4})(\d{4})/, '$1-$2-$3');
    }
    return digits.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
  }
  
  return digits;
}

// 郵便番号フォーマット
function formatPostalCode(value) {
  const digits = value.replace(/\D/g, '');
  if (digits.length === 7) {
    return digits.replace(/(\d{3})(\d{4})/, '$1-$2');
  }
  return digits;
}

// 入力時の自動フォーマット
function setupAutoFormat(inputSelector, formatter) {
  const input = $(inputSelector);
  if (!input) return;
  
  input.addEventListener('input', (e) => {
    const cursorPos = e.target.selectionStart;
    const oldLength = e.target.value.length;
    e.target.value = formatter(e.target.value);
    const newLength = e.target.value.length;
    
    // カーソル位置を調整
    const newCursorPos = cursorPos + (newLength - oldLength);
    e.target.setSelectionRange(newCursorPos, newCursorPos);
  });
}

// 使用例
setupAutoFormat('#phone', formatPhoneNumber);
setupAutoFormat('#postal-code', formatPostalCode);

10. 配列・オブジェクト操作

配列操作

// 重複削除
function unique(arr) {
  return [...new Set(arr)];
}

// オブジェクト配列の重複削除(キー指定)
function uniqueBy(arr, key) {
  const seen = new Set();
  return arr.filter(item => {
    const value = typeof key === 'function' ? key(item) : item[key];
    if (seen.has(value)) return false;
    seen.add(value);
    return true;
  });
}

// 配列のシャッフル
function shuffle(arr) {
  const result = [...arr];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

// チャンク分割
function chunk(arr, size) {
  const result = [];
  for (let i = 0; i < arr.length; i += size) {
    result.push(arr.slice(i, i + size));
  }
  return result;
}

// グルーピング
function groupBy(arr, key) {
  return arr.reduce((groups, item) => {
    const value = typeof key === 'function' ? key(item) : item[key];
    (groups[value] = groups[value] || []).push(item);
    return groups;
  }, {});
}

// 日本語対応ソート
function sortByJapanese(arr, key = null) {
  const collator = new Intl.Collator('ja');
  return [...arr].sort((a, b) => {
    const valueA = key ? a[key] : a;
    const valueB = key ? b[key] : b;
    return collator.compare(valueA, valueB);
  });
}

// 配列の最後からN個取得
function takeLast(arr, n = 1) {
  return arr.slice(-n);
}

// 配列の差分
function difference(arr1, arr2) {
  const set2 = new Set(arr2);
  return arr1.filter(item => !set2.has(item));
}

// 配列の交差
function intersection(arr1, arr2) {
  const set2 = new Set(arr2);
  return arr1.filter(item => set2.has(item));
}

オブジェクト操作

// 深いコピー
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof Array) return obj.map(item => deepClone(item));
  if (obj instanceof Object) {
    const copy = {};
    Object.keys(obj).forEach(key => {
      copy[key] = deepClone(obj[key]);
    });
    return copy;
  }
}

// 深いマージ
function deepMerge(target, source) {
  const result = { ...target };
  
  for (const key of Object.keys(source)) {
    if (source[key] instanceof Object && key in target) {
      result[key] = deepMerge(target[key], source[key]);
    } else {
      result[key] = source[key];
    }
  }
  
  return result;
}

// 特定のキーだけ抽出
function pick(obj, keys) {
  return keys.reduce((result, key) => {
    if (key in obj) result[key] = obj[key];
    return result;
  }, {});
}

// 特定のキーを除外
function omit(obj, keys) {
  const result = { ...obj };
  keys.forEach(key => delete result[key]);
  return result;
}

// ネストしたプロパティを安全に取得
function get(obj, path, defaultValue = undefined) {
  const keys = path.split('.');
  let result = obj;
  
  for (const key of keys) {
    if (result == null) return defaultValue;
    result = result[key];
  }
  
  return result === undefined ? defaultValue : result;
}

// ネストしたプロパティを設定
function set(obj, path, value) {
  const keys = path.split('.');
  let current = obj;
  
  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];
    if (!(key in current)) current[key] = {};
    current = current[key];
  }
  
  current[keys[keys.length - 1]] = value;
  return obj;
}

// オブジェクトが空かどうか
function isEmpty(obj) {
  if (obj == null) return true;
  if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
  return Object.keys(obj).length === 0;
}

// 使用例
const user = { profile: { name: '山田', address: { city: '東京' } } };
get(user, 'profile.address.city');           // '東京'
get(user, 'profile.phone', '未設定');         // '未設定'

11. 画像・メディア

遅延読み込み

// ネイティブLazy Loadのフォールバック
function setupLazyLoad(selector = 'img[data-src]') {
  // ネイティブサポートがあればそちらを使用
  if ('loading' in HTMLImageElement.prototype) {
    $$(selector).forEach(img => {
      img.src = img.dataset.src;
      if (img.dataset.srcset) {
        img.srcset = img.dataset.srcset;
      }
      img.loading = 'lazy';
    });
    return;
  }
  
  // IntersectionObserver によるフォールバック
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        if (img.dataset.srcset) {
          img.srcset = img.dataset.srcset;
        }
        img.classList.add('is-loaded');
        observer.unobserve(img);
      }
    });
  }, {
    rootMargin: '50px 0px'
  });
  
  $$(selector).forEach(img => observer.observe(img));
}

画像読み込み検知

// 画像の読み込み完了を待つ
function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

// 複数画像の読み込みを待つ
async function loadImages(sources) {
  return Promise.all(sources.map(src => loadImage(src)));
}

// 画像がすでに読み込まれているかチェック
function isImageLoaded(img) {
  return img.complete && img.naturalHeight !== 0;
}

// ページ内のすべての画像が読み込まれるのを待つ
function waitForAllImages() {
  const images = $$('img');
  const promises = images.map(img => {
    if (isImageLoaded(img)) return Promise.resolve();
    return new Promise(resolve => {
      img.onload = resolve;
      img.onerror = resolve;
    });
  });
  return Promise.all(promises);
}

WebP対応チェック

// WebPサポートチェック
function supportsWebP() {
  return new Promise(resolve => {
    const img = new Image();
    img.onload = () => resolve(img.width === 1);
    img.onerror = () => resolve(false);
    img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
  });
}

// 使用例
supportsWebP().then(supported => {
  document.documentElement.classList.add(supported ? 'webp' : 'no-webp');
});

アスペクト比計算

// アスペクト比を維持したリサイズ計算
function calculateAspectRatio(originalWidth, originalHeight, maxWidth, maxHeight) {
  let width = originalWidth;
  let height = originalHeight;
  
  if (width > maxWidth) {
    height = (maxWidth / width) * height;
    width = maxWidth;
  }
  
  if (height > maxHeight) {
    width = (maxHeight / height) * width;
    height = maxHeight;
  }
  
  return { width: Math.round(width), height: Math.round(height) };
}

// 画像のアスペクト比を取得
function getImageAspectRatio(img) {
  return img.naturalWidth / img.naturalHeight;
}

12. クリップボード

テキストコピー

// テキストをクリップボードにコピー
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    // フォールバック(古いブラウザ用)
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    document.body.appendChild(textarea);
    textarea.select();
    
    try {
      document.execCommand('copy');
      return true;
    } finally {
      document.body.removeChild(textarea);
    }
  }
}

// コピーボタンのセットアップ
function setupCopyButton(buttonSelector, textSource) {
  const button = $(buttonSelector);
  if (!button) return;
  
  button.addEventListener('click', async () => {
    const text = typeof textSource === 'function' 
      ? textSource() 
      : $(textSource)?.textContent || textSource;
    
    const success = await copyToClipboard(text);
    
    // フィードバック表示
    const originalText = button.textContent;
    button.textContent = success ? 'コピーしました!' : 'コピー失敗';
    button.disabled = true;
    
    setTimeout(() => {
      button.textContent = originalText;
      button.disabled = false;
    }, 2000);
  });
}

// 使用例
setupCopyButton('#copy-btn', '#code-block');

クリップボードから読み取り

// クリップボードからテキストを読み取り
async function readFromClipboard() {
  try {
    return await navigator.clipboard.readText();
  } catch (error) {
    console.error('クリップボードの読み取りに失敗:', error);
    return null;
  }
}

13. デバイス・環境検知

デバイス判定

// タッチデバイス判定
function isTouchDevice() {
  return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}

// iOS判定
function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}

// Android判定
function isAndroid() {
  return /Android/.test(navigator.userAgent);
}

// モバイル判定
function isMobile() {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}

// Safari判定
function isSafari() {
  return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}

環境・設定検知

// ダークモード検知
function isDarkMode() {
  return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

// ダークモード変更を監視
function watchDarkMode(callback) {
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  mediaQuery.addEventListener('change', (e) => callback(e.matches));
  return () => mediaQuery.removeEventListener('change', callback);
}

// 視覚効果軽減設定の検知
function prefersReducedMotion() {
  return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

// オンライン/オフライン状態
function isOnline() {
  return navigator.onLine;
}

// オンライン状態の変化を監視
function watchOnlineStatus(callback) {
  const onlineHandler = () => callback(true);
  const offlineHandler = () => callback(false);
  
  window.addEventListener('online', onlineHandler);
  window.addEventListener('offline', offlineHandler);
  
  return () => {
    window.removeEventListener('online', onlineHandler);
    window.removeEventListener('offline', offlineHandler);
  };
}

// ブレークポイント検知
function getBreakpoint() {
  const width = window.innerWidth;
  if (width < 576) return 'xs';
  if (width < 768) return 'sm';
  if (width < 992) return 'md';
  if (width < 1200) return 'lg';
  return 'xl';
}

// ブレークポイント変更を監視
function watchBreakpoint(callback) {
  let currentBreakpoint = getBreakpoint();
  
  const handler = debounce(() => {
    const newBreakpoint = getBreakpoint();
    if (newBreakpoint !== currentBreakpoint) {
      callback(newBreakpoint, currentBreakpoint);
      currentBreakpoint = newBreakpoint;
    }
  }, 100);
  
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}

14. UI部品

アコーディオン

// アコーディオンのセットアップ
function setupAccordion(containerSelector, options = {}) {
  const container = $(containerSelector);
  if (!container) return;
  
  const {
    triggerSelector = '.accordion-trigger',
    contentSelector = '.accordion-content',
    activeClass = 'is-active',
    allowMultiple = false
  } = options;
  
  const triggers = $$(triggerSelector, container);
  
  triggers.forEach(trigger => {
    trigger.addEventListener('click', () => {
      const isActive = trigger.classList.contains(activeClass);
      const content = trigger.nextElementSibling;
      
      // 他を閉じる(allowMultiple=falseの場合)
      if (!allowMultiple && !isActive) {
        triggers.forEach(t => {
          t.classList.remove(activeClass);
          const c = t.nextElementSibling;
          if (c) c.style.maxHeight = null;
        });
      }
      
      // トグル
      trigger.classList.toggle(activeClass);
      if (content) {
        if (trigger.classList.contains(activeClass)) {
          content.style.maxHeight = content.scrollHeight + 'px';
        } else {
          content.style.maxHeight = null;
        }
      }
    });
  });
}

// CSS
// .accordion-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }

タブ

// タブのセットアップ
function setupTabs(containerSelector, options = {}) {
  const container = $(containerSelector);
  if (!container) return;
  
  const {
    tabSelector = '.tab',
    panelSelector = '.tab-panel',
    activeClass = 'is-active'
  } = options;
  
  const tabs = $$(tabSelector, container);
  const panels = $$(panelSelector, container);
  
  function activateTab(index) {
    tabs.forEach((tab, i) => {
      tab.classList.toggle(activeClass, i === index);
      tab.setAttribute('aria-selected', i === index);
    });
    
    panels.forEach((panel, i) => {
      panel.classList.toggle(activeClass, i === index);
      panel.hidden = i !== index;
    });
  }
  
  tabs.forEach((tab, index) => {
    tab.addEventListener('click', () => activateTab(index));
    
    // キーボード操作
    tab.addEventListener('keydown', (e) => {
      let newIndex = index;
      
      if (e.key === 'ArrowRight') {
        newIndex = (index + 1) % tabs.length;
      } else if (e.key === 'ArrowLeft') {
        newIndex = (index - 1 + tabs.length) % tabs.length;
      }
      
      if (newIndex !== index) {
        activateTab(newIndex);
        tabs[newIndex].focus();
      }
    });
  });
}

モーダル

// モーダル制御
function createModal(modalSelector) {
  const modal = $(modalSelector);
  if (!modal) return null;
  
  const closeButtons = $$('[data-modal-close]', modal);
  let cleanup = null;
  
  function open() {
    modal.classList.add('is-open');
    modal.setAttribute('aria-hidden', 'false');
    scrollLock.enable();
    
    // ESCキーで閉じる
    const escHandler = (e) => {
      if (e.key === 'Escape') close();
    };
    document.addEventListener('keydown', escHandler);
    
    // 外側クリックで閉じる
    const backdropHandler = (e) => {
      if (e.target === modal) close();
    };
    modal.addEventListener('click', backdropHandler);
    
    cleanup = () => {
      document.removeEventListener('keydown', escHandler);
      modal.removeEventListener('click', backdropHandler);
    };
    
    // フォーカストラップ
    trapFocus(modal);
  }
  
  function close() {
    modal.classList.remove('is-open');
    modal.setAttribute('aria-hidden', 'true');
    scrollLock.disable();
    
    if (cleanup) {
      cleanup();
      cleanup = null;
    }
  }
  
  closeButtons.forEach(btn => {
    btn.addEventListener('click', close);
  });
  
  return { open, close };
}

// 使用例
const modal = createModal('#my-modal');
$('#open-modal-btn').addEventListener('click', modal.open);

ハンバーガーメニュー

// ハンバーガーメニュー
function setupHamburgerMenu(options = {}) {
  const {
    triggerSelector = '.hamburger',
    menuSelector = '.nav-menu',
    activeClass = 'is-open',
    bodyActiveClass = 'menu-open'
  } = options;
  
  const trigger = $(triggerSelector);
  const menu = $(menuSelector);
  if (!trigger || !menu) return;
  
  let isOpen = false;
  
  function toggle() {
    isOpen = !isOpen;
    trigger.classList.toggle(activeClass, isOpen);
    menu.classList.toggle(activeClass, isOpen);
    document.body.classList.toggle(bodyActiveClass, isOpen);
    trigger.setAttribute('aria-expanded', isOpen);
    
    if (isOpen) {
      scrollLock.enable();
    } else {
      scrollLock.disable();
    }
  }
  
  function close() {
    if (!isOpen) return;
    isOpen = false;
    trigger.classList.remove(activeClass);
    menu.classList.remove(activeClass);
    document.body.classList.remove(bodyActiveClass);
    trigger.setAttribute('aria-expanded', 'false');
    scrollLock.disable();
  }
  
  trigger.addEventListener('click', toggle);
  
  // メニュー内リンククリックで閉じる
  $$('a', menu).forEach(link => {
    link.addEventListener('click', close);
  });
  
  // リサイズ時に閉じる
  window.addEventListener('resize', debounce(() => {
    if (window.innerWidth > 768) close();
  }, 100));
  
  return { toggle, close };
}

ドロップダウン

// ドロップダウン
function setupDropdown(triggerSelector, options = {}) {
  const trigger = $(triggerSelector);
  if (!trigger) return;
  
  const {
    menuSelector = trigger.nextElementSibling,
    activeClass = 'is-open',
    closeOnClickOutside = true,
    closeOnSelect = true
  } = options;
  
  const menu = typeof menuSelector === 'string' ? $(menuSelector) : menuSelector;
  if (!menu) return;
  
  let isOpen = false;
  let cleanup = null;
  
  function open() {
    isOpen = true;
    trigger.classList.add(activeClass);
    menu.classList.add(activeClass);
    trigger.setAttribute('aria-expanded', 'true');
    
    if (closeOnClickOutside) {
      cleanup = onClickOutside(trigger.parentElement, close);
    }
  }
  
  function close() {
    isOpen = false;
    trigger.classList.remove(activeClass);
    menu.classList.remove(activeClass);
    trigger.setAttribute('aria-expanded', 'false');
    
    if (cleanup) {
      cleanup();
      cleanup = null;
    }
  }
  
  function toggle() {
    isOpen ? close() : open();
  }
  
  trigger.addEventListener('click', (e) => {
    e.stopPropagation();
    toggle();
  });
  
  if (closeOnSelect) {
    $$('a, button', menu).forEach(item => {
      item.addEventListener('click', close);
    });
  }
  
  return { open, close, toggle };
}

15. アクセシビリティ

フォーカストラップ

// モーダル内にフォーカスを閉じ込める
function trapFocus(element) {
  const focusableElements = element.querySelectorAll(
    'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
  );
  
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];
  
  function handleKeyDown(e) {
    if (e.key !== 'Tab') return;
    
    if (e.shiftKey) {
      if (document.activeElement === firstElement) {
        lastElement.focus();
        e.preventDefault();
      }
    } else {
      if (document.activeElement === lastElement) {
        firstElement.focus();
        e.preventDefault();
      }
    }
  }
  
  element.addEventListener('keydown', handleKeyDown);
  firstElement?.focus();
  
  return () => element.removeEventListener('keydown', handleKeyDown);
}

ライブリージョン

// スクリーンリーダー向け通知
function announce(message, priority = 'polite') {
  let announcer = $('#sr-announcer');
  
  if (!announcer) {
    announcer = document.createElement('div');
    announcer.id = 'sr-announcer';
    announcer.setAttribute('aria-live', priority);
    announcer.setAttribute('aria-atomic', 'true');
    announcer.className = 'sr-only';
    document.body.appendChild(announcer);
  }
  
  announcer.setAttribute('aria-live', priority);
  announcer.textContent = '';
  
  // 少し遅延させて確実に読み上げさせる
  setTimeout(() => {
    announcer.textContent = message;
  }, 100);
}

// CSS
// .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

// 使用例
announce('フォームを送信しました', 'assertive');

キーボードナビゲーション

// 矢印キーでのナビゲーション
function setupArrowNavigation(containerSelector, itemSelector) {
  const container = $(containerSelector);
  if (!container) return;
  
  container.addEventListener('keydown', (e) => {
    if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
      return;
    }
    
    const items = $$(itemSelector, container);
    const currentIndex = items.indexOf(document.activeElement);
    
    if (currentIndex === -1) return;
    
    e.preventDefault();
    let newIndex = currentIndex;
    
    switch (e.key) {
      case 'ArrowDown':
      case 'ArrowRight':
        newIndex = (currentIndex + 1) % items.length;
        break;
      case 'ArrowUp':
      case 'ArrowLeft':
        newIndex = (currentIndex - 1 + items.length) % items.length;
        break;
    }
    
    items[newIndex].focus();
  });
}

16. セキュリティ・サニタイズ

XSS対策

// HTMLエスケープ
function escapeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// HTMLエスケープ(より安全なバージョン)
function escapeHTMLStrict(str) {
  const escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
    '/': '&#x2F;',
    '`': '&#x60;',
    '=': '&#x3D;'
  };
  
  return str.replace(/[&<>"'`=/]/g, char => escapeMap[char]);
}

// 安全なinnerHTML設定
function setSafeHTML(element, html) {
  const el = typeof element === 'string' ? $(element) : element;
  if (!el) return;
  
  // DOMPurifyがあれば使用(推奨)
  if (typeof DOMPurify !== 'undefined') {
    el.innerHTML = DOMPurify.sanitize(html);
  } else {
    // 簡易的なサニタイズ
    el.innerHTML = html
      .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
      .replace(/on\w+="[^"]*"/gi, '')
      .replace(/on\w+='[^']*'/gi, '');
  }
}

入力サニタイズ

// 入力値のサニタイズ
function sanitizeInput(value, options = {}) {
  const {
    trim = true,
    toLowerCase = false,
    toUpperCase = false,
    maxLength = null,
    allowedChars = null,
    removeHTML = true
  } = options;
  
  let result = String(value);
  
  if (removeHTML) {
    result = result.replace(/<[^>]*>/g, '');
  }
  
  if (trim) {
    result = result.trim();
  }
  
  if (toLowerCase) {
    result = result.toLowerCase();
  }
  
  if (toUpperCase) {
    result = result.toUpperCase();
  }
  
  if (allowedChars) {
    result = result.replace(new RegExp(`[^${allowedChars}]`, 'g'), '');
  }
  
  if (maxLength) {
    result = result.slice(0, maxLength);
  }
  
  return result;
}

// 使用例
const email = sanitizeInput(input, { toLowerCase: true, trim: true });
const phone = sanitizeInput(input, { allowedChars: '0-9-' });

17. 印刷

特定要素の印刷

// 特定の要素だけを印刷
function printElement(selector) {
  const element = $(selector);
  if (!element) return;
  
  const originalContents = document.body.innerHTML;
  const printContents = element.innerHTML;
  
  document.body.innerHTML = printContents;
  window.print();
  document.body.innerHTML = originalContents;
  
  // イベントリスナーを再設定する必要がある場合は
  // ここでリロードまたは再初期化を行う
  window.location.reload();
}

// iframe を使った印刷(元のページを壊さない)
function printElementSafe(selector) {
  const element = $(selector);
  if (!element) return;
  
  const iframe = document.createElement('iframe');
  iframe.style.position = 'fixed';
  iframe.style.right = '0';
  iframe.style.bottom = '0';
  iframe.style.width = '0';
  iframe.style.height = '0';
  iframe.style.border = '0';
  
  document.body.appendChild(iframe);
  
  const doc = iframe.contentWindow.document;
  doc.open();
  doc.write(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>Print</title>
      <style>
        ${Array.from(document.styleSheets)
          .map(sheet => {
            try {
              return Array.from(sheet.cssRules)
                .map(rule => rule.cssText)
                .join('\n');
            } catch {
              return '';
            }
          })
          .join('\n')}
      </style>
    </head>
    <body>${element.innerHTML}</body>
    </html>
  `);
  doc.close();
  
  iframe.contentWindow.focus();
  iframe.contentWindow.print();
  
  setTimeout(() => {
    document.body.removeChild(iframe);
  }, 100);
}

印刷スタイルの切り替え

// 印刷時のスタイル制御
function setupPrintStyles() {
  window.addEventListener('beforeprint', () => {
    document.body.classList.add('is-printing');
    // 印刷前の処理(グラフの展開など)
  });
  
  window.addEventListener('afterprint', () => {
    document.body.classList.remove('is-printing');
    // 印刷後の処理
  });
}

// 印刷プレビュー用のクラスをトグル
function togglePrintPreview() {
  document.body.classList.toggle('print-preview');
}

18. ユーティリティ

ランダム生成

// ランダムID生成
function generateId(prefix = '', length = 8) {
  const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
  let id = '';
  for (let i = 0; i < length; i++) {
    id += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return prefix ? `${prefix}-${id}` : id;
}

// UUID v4 生成
function generateUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
    const r = Math.random() * 16 | 0;
    const v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

// ランダムな範囲の数値
function randomBetween(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// ランダムな色
function randomColor() {
  return '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
}

遅延・タイミング

// sleep関数
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用例
async function process() {
  console.log('開始');
  await sleep(1000);
  console.log('1秒後');
}

// 次のフレームまで待つ
function nextFrame() {
  return new Promise(resolve => requestAnimationFrame(resolve));
}

// アイドル時に実行
function runWhenIdle(callback, timeout = 1000) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(callback, { timeout });
  } else {
    setTimeout(callback, 1);
  }
}

条件分岐ヘルパー

// nullish coalescing のポリフィル的な関数
function ifNull(value, defaultValue) {
  return value ?? defaultValue;
}

// 条件付きでオブジェクトにプロパティを追加
function conditionalProps(condition, props) {
  return condition ? props : {};
}

// 使用例(React風)
const buttonProps = {
  className: 'btn',
  ...conditionalProps(isLoading, { disabled: true }),
  ...conditionalProps(variant === 'primary', { className: 'btn btn-primary' })
};

エラーハンドリング

// 安全な関数実行
function safely(fn, fallback = null) {
  try {
    return fn();
  } catch (error) {
    console.error('Error:', error);
    return fallback;
  }
}

// 非同期版
async function safelyAsync(fn, fallback = null) {
  try {
    return await fn();
  } catch (error) {
    console.error('Error:', error);
    return fallback;
  }
}

// 使用例
const data = safely(() => JSON.parse(jsonString), {});
const user = await safelyAsync(() => fetchAPI('/api/user'), null);

デバッグヘルパー

// 条件付きログ
const isDev = window.location.hostname === 'localhost';

const logger = {
  log: (...args) => isDev && console.log(...args),
  warn: (...args) => isDev && console.warn(...args),
  error: (...args) => console.error(...args), // エラーは常に出力
  table: (...args) => isDev && console.table(...args),
  time: (label) => isDev && console.time(label),
  timeEnd: (label) => isDev && console.timeEnd(label)
};

// パフォーマンス計測
function measureTime(label, fn) {
  console.time(label);
  const result = fn();
  console.timeEnd(label);
  return result;
}

async function measureTimeAsync(label, fn) {
  console.time(label);
  const result = await fn();
  console.timeEnd(label);
  return result;
}

最後に

このスニペット集は実務で頻繁に使用するパターンをまとめたものです。

使用上の注意

  1. 存在チェックを忘れずに - DOM要素は必ず存在確認を行う
  2. パフォーマンスを意識 - スクロール/リサイズイベントにはデバウンス/スロットルを適用
  3. アクセシビリティを確保 - キーボード操作、スクリーンリーダー対応を忘れずに
  4. ブラウザサポートを確認 - 古いブラウザ対応が必要な場合はポリフィルを検討

推奨構成

/js
  /utils
    dom.js        # DOM操作
    events.js     # イベント制御
    format.js     # フォーマット
    validate.js   # バリデーション
    storage.js    # ストレージ
    api.js        # API通信
  /components
    modal.js      # モーダル
    tabs.js       # タブ
    accordion.js  # アコーディオン
  main.js         # エントリーポイント

プロジェクトに合わせてカスタマイズしてご活用ください。

この記事をシェア