確認ダイアログの実装
kintone で業務アプリを運用していると、「重要な操作の前に確認を挟みたい」「処理の完了をユーザーに分かりやすく伝えたい」といった要件が出てきます。標準の window.confirm は手軽ですが、見た目が素っ気なく情報量にも限界があります。
この記事では、kintone カスタマイズにおける確認ダイアログと**カスタム通知(トースト)**の実装パターンを紹介します。ブラウザ標準のダイアログから、CSS を使ったモーダルダイアログ、処理結果の通知表示まで、段階的に解説します。
ブラウザ標準の confirm による確認
保存前の確認ダイアログ
最もシンプルな方法として、submit イベント内で window.confirm を使う方法があります。
(() => {
'use strict';
const events = [
'app.record.create.submit',
'app.record.edit.submit',
];
kintone.events.on(events, (event) => {
const record = event.record;
const amount = Number(record['金額'].value) || 0;
// 金額が100万円を超える場合に確認
if (amount > 1000000) {
const confirmed = confirm(
`金額が${amount.toLocaleString()}円です。このまま保存しますか?`
);
if (!confirmed) {
event.error = '保存がキャンセルされました';
}
}
return event;
});
})();
confirm が false を返した場合、event.error
にメッセージを設定することで保存をキャンセルできます。ユーザーには画面上部にエラーメッセージとして表示されます。
プロセス管理のアクション実行前に確認
承認や差し戻しなどの重要な操作前に確認を入れるパターンです。
(() => {
'use strict';
const events = [
'app.record.detail.process.proceed',
'mobile.app.record.detail.process.proceed',
];
kintone.events.on(events, (event) => {
const action = event.action.value;
const nextStatus = event.nextStatus.value;
// 差し戻しアクションの場合に確認
if (action === '差し戻す') {
const confirmed = confirm(
`ステータスを「${nextStatus}」に変更します。差し戻してよろしいですか?`
);
if (!confirmed) {
event.error = 'アクションがキャンセルされました';
}
}
return event;
});
})();
カスタムモーダルダイアログ
CSS で装飾したモーダルダイアログ
標準の confirm よりもリッチな見た目で、追加情報やアイコンを含むモーダルダイアログを表示するパターンです。
(() => {
'use strict';
/**
* カスタム確認ダイアログを表示する
* @param { Object } options
* @param { string } options.title - ダイアログのタイトル
* @param { string } options.message - メッセージ本文
* @param { string } [options.confirmText] - 確認ボタンのテキスト(デフォルト: OK)
* @param { string } [options.cancelText] - キャンセルボタンのテキスト(デフォルト: キャンセル)
* @returns { Promise<boolean> } ユーザーの選択結果
*/
const showConfirmDialog = ({ title, message, confirmText = 'OK', cancelText = 'キャンセル' }) => {
return new Promise((resolve) => {
// オーバーレイ
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5); z-index: 10000;
display: flex; align-items: center; justify-content: center;
`;
// ダイアログ本体
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #fff; border-radius: 8px; padding: 24px;
max-width: 420px; width: 90%; box-shadow: 0 4px 24px rgba(0,0,0,0.2);
`;
// タイトル
const titleEl = document.createElement('h3');
titleEl.textContent = title;
titleEl.style.cssText = 'margin: 0 0 12px; font-size: 16px; color: #333;';
// メッセージ
const messageEl = document.createElement('p');
messageEl.textContent = message;
messageEl.style.cssText = 'margin: 0 0 20px; font-size: 14px; color: #666; line-height: 1.6;';
// ボタンコンテナ
const buttons = document.createElement('div');
buttons.style.cssText = 'display: flex; justify-content: flex-end; gap: 8px;';
// キャンセルボタン
const cancelBtn = document.createElement('button');
cancelBtn.textContent = cancelText;
cancelBtn.style.cssText = `
padding: 8px 16px; border: 1px solid #ccc; border-radius: 4px;
background: #fff; cursor: pointer; font-size: 14px;
`;
cancelBtn.addEventListener('click', () => {
overlay.remove();
resolve(false);
});
// 確認ボタン
const confirmBtn = document.createElement('button');
confirmBtn.textContent = confirmText;
confirmBtn.style.cssText = `
padding: 8px 16px; border: none; border-radius: 4px;
background: #3498db; color: #fff; cursor: pointer; font-size: 14px;
`;
confirmBtn.addEventListener('click', () => {
overlay.remove();
resolve(true);
});
buttons.appendChild(cancelBtn);
buttons.appendChild(confirmBtn);
dialog.appendChild(titleEl);
dialog.appendChild(messageEl);
dialog.appendChild(buttons);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// ESC キーでキャンセル
const handleKeydown = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handleKeydown);
resolve(false);
}
};
document.addEventListener('keydown', handleKeydown);
});
};
// 使用例: ボタンクリック時にカスタムダイアログを表示
const events = ['app.record.detail.show'];
kintone.events.on(events, (event) => {
const space = kintone.app.record.getSpaceElement('アクションボタン');
if (!space || space.childNodes.length > 0) return event;
const button = document.createElement('button');
button.textContent = '一括処理を実行';
button.className = 'kintoneplugin-button-dialog-ok';
button.addEventListener('click', async () => {
const confirmed = await showConfirmDialog({
title: '一括処理の確認',
message: 'この操作を実行すると、関連する全てのレコードが更新されます。この操作は取り消せません。',
confirmText: '実行する',
cancelText: 'やめる',
});
if (confirmed) {
// 一括処理の実行
alert('処理を開始します');
}
});
space.appendChild(button);
return event;
});
})();
showConfirmDialog は Promise を返すので、async/await
で結果を受け取れます。これにより、既存のコードにカスタムダイアログを追加する際も、confirm
を置き換えるだけで済みます。
トースト通知(処理完了通知)
画面右上にフェードイン・フェードアウトする通知
処理の完了や軽微なエラーをユーザーに通知するための、トースト型通知コンポーネントです。
(() => {
'use strict';
/**
* トースト通知を表示する
* @param { Object } options
* @param { string } options.message - 通知メッセージ
* @param { 'success' | 'error' | 'info' | 'warning' } [options.type] - 通知タイプ
* @param { number } [options.duration] - 表示時間(ミリ秒、デフォルト: 3000)
*/
const showToast = ({ message, type = 'info', duration = 3000 }) => {
const colors = {
success: { bg: '#d4edda', border: '#28a745', text: '#155724' },
error: { bg: '#f8d7da', border: '#dc3545', text: '#721c24' },
info: { bg: '#d1ecf1', border: '#17a2b8', text: '#0c5460' },
warning: { bg: '#fff3cd', border: '#ffc107', text: '#856404' },
};
const color = colors[type] || colors.info;
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed; top: 16px; right: 16px; z-index: 10001;
padding: 12px 20px; border-radius: 6px; font-size: 14px;
background: ${color.bg}; border-left: 4px solid ${color.border}; color: ${color.text};
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
transition: opacity 0.3s ease, transform 0.3s ease;
opacity: 0; transform: translateX(20px);
`;
document.body.appendChild(toast);
// フェードイン
requestAnimationFrame(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
});
// 指定時間後にフェードアウト
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(20px)';
setTimeout(() => toast.remove(), 300);
}, duration);
};
// 使用例: レコード保存成功時に通知
const successEvents = [
'app.record.create.submit.success',
'app.record.edit.submit.success',
];
kintone.events.on(successEvents, (event) => {
showToast({
message: 'レコードを保存しました',
type: 'success',
duration: 3000,
});
return event;
});
})();
複数の通知をスタック表示
複数のトースト通知を同時に表示する場合、重ならないようにスタック(積み重ね)表示する実装です。
(() => {
'use strict';
// トーストコンテナを作成
const createContainer = () => {
let container = document.getElementById('kintone-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'kintone-toast-container';
container.style.cssText = `
position: fixed; top: 16px; right: 16px; z-index: 10001;
display: flex; flex-direction: column; gap: 8px;
`;
document.body.appendChild(container);
}
return container;
};
/**
* スタック対応のトースト通知を表示する
* @param { string } message - 通知メッセージ
* @param { 'success' | 'error' | 'info' | 'warning' } [type] - 通知タイプ
*/
const showToast = (message, type = 'info') => {
const container = createContainer();
const colors = {
success: { bg: '#d4edda', border: '#28a745', text: '#155724', icon: '✓' },
error: { bg: '#f8d7da', border: '#dc3545', text: '#721c24', icon: '✕' },
info: { bg: '#d1ecf1', border: '#17a2b8', text: '#0c5460', icon: 'ℹ' },
warning: { bg: '#fff3cd', border: '#ffc107', text: '#856404', icon: '⚠' },
};
const color = colors[type] || colors.info;
const toast = document.createElement('div');
toast.innerHTML = `<span style="margin-right: 8px; font-weight: bold;">${color.icon}</span>${message}`;
toast.style.cssText = `
padding: 12px 20px; border-radius: 6px; font-size: 14px;
background: ${color.bg}; border-left: 4px solid ${color.border}; color: ${color.text};
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
transition: opacity 0.3s ease; opacity: 0; cursor: pointer;
`;
// クリックで閉じる
toast.addEventListener('click', () => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
});
container.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = '1';
});
// 5秒後に自動消去
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 5000);
};
// グローバルに公開(他のスクリプトから利用可能にする)
window.kintoneToast = showToast;
})();
window.kintoneToast としてグローバルに公開することで、他のカスタマイズスクリプトからも
kintoneToast('メッセージ', 'success') のように呼び出せます。
クリップボードへのコピー通知
レコード ID やフィールド値をクリップボードにコピーし、コピー完了をトーストで通知する実用的な例です。
(() => {
'use strict';
const showToast = (message, type = 'info') => {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed; top: 16px; right: 16px; z-index: 10001;
padding: 12px 20px; border-radius: 6px; font-size: 14px;
background: ${type === 'success' ? '#d4edda' : '#f8d7da'};
color: ${type === 'success' ? '#155724' : '#721c24'};
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
transition: opacity 0.3s; opacity: 0;
`;
document.body.appendChild(toast);
requestAnimationFrame(() => { toast.style.opacity = '1'; });
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 2000);
};
/**
* テキストをクリップボードにコピーする
* @param { string } text - コピーするテキスト
* @returns { Promise<void> }
*/
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
showToast('クリップボードにコピーしました', 'success');
} catch {
// Clipboard API が使えない場合のフォールバック
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.cssText = 'position: fixed; top: -9999px;';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
showToast('クリップボードにコピーしました', 'success');
}
};
const events = ['app.record.detail.show'];
kintone.events.on(events, (event) => {
const space = kintone.app.record.getSpaceElement('コピーボタン');
if (!space || space.childNodes.length > 0) return event;
// レコード ID コピーボタン
const idButton = document.createElement('button');
idButton.textContent = 'ID をコピー';
idButton.className = 'kintoneplugin-button-normal';
idButton.addEventListener('click', () => {
const recordId = kintone.app.record.getId();
copyToClipboard(String(recordId));
});
// レコード URL コピーボタン
const urlButton = document.createElement('button');
urlButton.textContent = 'URL をコピー';
urlButton.className = 'kintoneplugin-button-normal';
urlButton.style.marginLeft = '8px';
urlButton.addEventListener('click', () => {
copyToClipboard(location.href);
});
space.appendChild(idButton);
space.appendChild(urlButton);
return event;
});
})();
navigator.clipboard.writeText は HTTPS 環境(または localhost)でのみ動作します。kintone は
HTTPS で提供されるため通常は問題ありませんが、念のため document.execCommand('copy')
によるフォールバックを実装しています。
まとめ
- 標準の
window.confirmは手軽だが、表示情報に限界がある - カスタムモーダルダイアログは
Promiseを返す設計にするとasync/awaitで扱いやすい - ESC キーやオーバーレイクリックでの閉じる操作も実装すると使い勝手が向上する
- トースト通知は
position: fixed+ CSS transition で簡潔に実装できる - 複数通知のスタック表示には、コンテナ要素を使って
flex-direction: columnで配置する navigator.clipboard.writeTextを使えば、フィールド値やレコード URL のコピーが容易- Clipboard API が使えない環境のフォールバックとして
document.execCommand('copy')を用意する