フロントエンド開発でよく使う機能を厳選。DOM操作、フォーム処理、アニメーションなど、コピペで使えるJavaScriptコードをクライアントワークの実例と共に紹介します。
はじめに
「また同じ処理を書いている…」「前にも作ったはずなのに、どこにあるか分からない…」
Web制作の現場では、こうした場面に何度も遭遇します。本スニペット集は、実際のクライアントワークで繰り返し使用してきた実用的なJavaScriptコードを体系的にまとめたものです。
このスニペット集の特徴
- 実務で検証済み - 実案件で使用し、動作確認されたコードのみを収録
- コピペで即使用 - 依存ライブラリなし、そのまま使える純粋なJavaScript
- エラーに強い - null/undefined チェック、try-catch など防御的な実装
- モダンな記法 - ES6+の構文を採用(必要に応じてトランスパイル)
対象読者
- JavaScriptの基本は理解しているが、実務パターンを増やしたい方
- jQueryから脱却し、Vanilla JSに移行したい方
- チーム内で共通のユーティリティ関数を整備したい方
- 毎回ググるのをやめて、手元にリファレンスを持ちたい方
使い方
- 必要なスニペットを見つけて、プロジェクトにコピー
- 用途に合わせて関数名やオプションをカスタマイズ
- 複数のスニペットを組み合わせて、より複雑な機能を構築
推奨ディレクトリ構成:
/js
/utils ... 汎用ユーティリティ(DOM操作、フォーマットなど)
/components ... UI部品(モーダル、タブなど)
main.js ... エントリーポイント
動作環境
- モダンブラウザ(Chrome, Firefox, Safari, Edge 最新版)
- IE11非対応(必要な場合はBabelでトランスパイル)
目次
- DOM操作
- フォーム処理
- イベント制御
- スクロール・アニメーション
- API・非同期処理
- URL・パラメータ操作
- ストレージ・Cookie
- 日付・時間
- 数値・文字列フォーマット
- 配列・オブジェクト操作
- 画像・メディア
- クリップボード
- デバイス・環境検知
- UI部品
- アクセシビリティ
- セキュリティ・サニタイズ
- 印刷
- ユーティリティ
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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
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;
}
最後に
このスニペット集は実務で頻繁に使用するパターンをまとめたものです。
使用上の注意
- 存在チェックを忘れずに - DOM要素は必ず存在確認を行う
- パフォーマンスを意識 - スクロール/リサイズイベントにはデバウンス/スロットルを適用
- アクセシビリティを確保 - キーボード操作、スクリーンリーダー対応を忘れずに
- ブラウザサポートを確認 - 古いブラウザ対応が必要な場合はポリフィルを検討
推奨構成
/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 # エントリーポイント
プロジェクトに合わせてカスタマイズしてご活用ください。