Skip to content

Instantly share code, notes, and snippets.

@nnao45
Last active July 15, 2025 03:23
Show Gist options
  • Select an option

  • Save nnao45/f74613dc2971029e31225dcd2bf5a511 to your computer and use it in GitHub Desktop.

Select an option

Save nnao45/f74613dc2971029e31225dcd2bf5a511 to your computer and use it in GitHub Desktop.
ag-grid community cell selection
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ag-Grid サンプルアプリケーション</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ag-grid/31.0.1/ag-grid-community.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ag-grid/31.0.1/themes/ag-theme-alpine.min.css">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #0056b3;
}
.btn-danger {
background: #dc3545;
}
.btn-danger:hover {
background: #c82333;
}
.btn-success {
background: #28a745;
}
.btn-success:hover {
background: #218838;
}
#myGrid {
height: 500px;
width: 100%;
}
.stats {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.input-group {
display: flex;
gap: 10px;
align-items: center;
}
input {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* セル選択のスタイル */
.selected-cell {
background-color: #e3f2fd !important;
border: 2px solid #2196f3 !important;
}
.range-selecting {
background-color: #f3e5f5 !important;
border: 1px dashed #9c27b0 !important;
}
.range-selected {
background-color: #e8f5e8 !important;
border: 1px solid #4caf50 !important;
}
.selection-start {
background-color: #fff3e0 !important;
border: 2px solid #ff9800 !important;
}
.selection-end {
background-color: #fce4ec !important;
border: 2px solid #e91e63 !important;
}
.selected-info {
margin-top: 10px;
padding: 10px;
background: #e8f5e8;
border-radius: 4px;
border-left: 4px solid #4caf50;
}
/* 範囲選択中のカーソル */
.ag-root-wrapper.selecting {
cursor: crosshair !important;
}
.ag-root-wrapper.selecting * {
cursor: crosshair !important;
}
</style>
</head>
<body>
<div class="container">
<h1>🏢 社員管理システム - ag-Grid デモ</h1>
<div class="controls">
<button class="btn btn-success" onclick="addNewRow()">新規追加</button>
<button class="btn btn-danger" onclick="deleteSelected()">選択削除</button>
<button class="btn" onclick="exportToCsv()">CSV出力</button>
<button class="btn" onclick="clearFilters()">フィルタクリア</button>
<button class="btn" onclick="clearCellSelection()">セル選択クリア</button>
<button class="btn" onclick="copySelectedCellsToClipboard()">選択セルコピー</button>
<div class="input-group">
<label>クイック検索:</label>
<input type="text" id="quickSearch" placeholder="検索..." oninput="onQuickSearchChange()">
</div>
</div>
<div id="myGrid" class="ag-theme-alpine"></div>
<div class="stats">
<strong>統計情報:</strong>
<span id="statsInfo">データを読み込み中...</span>
</div>
<div class="selected-info" id="selectedCellInfo" style="display: none;">
<strong>選択されたセル:</strong>
<span id="cellDetails"></span>
<br>
<small style="color: #666; margin-top: 5px; display: block;">
💡 ヒント: Ctrl+C (Mac: Cmd+C) で選択したセルをコピーしてExcelに貼り付けできます
</small>
</div>
</div>
<script>
// サンプルデータ
const sampleData = [
{ id: 1, name: '田中太郎', department: '営業部', position: '主任', age: 28, salary: 4500000, joinDate: '2020-04-01', email: 'tanaka@company.com' },
{ id: 2, name: '佐藤花子', department: '開発部', position: '係長', age: 32, salary: 5200000, joinDate: '2018-07-15', email: 'sato@company.com' },
{ id: 3, name: '鈴木一郎', department: '人事部', position: '課長', age: 38, salary: 6000000, joinDate: '2015-03-10', email: 'suzuki@company.com' },
{ id: 4, name: '高橋美咲', department: '営業部', position: '主任', age: 29, salary: 4700000, joinDate: '2019-08-20', email: 'takahashi@company.com' },
{ id: 5, name: '伊藤健太', department: '開発部', position: '主任', age: 26, salary: 4300000, joinDate: '2021-06-01', email: 'ito@company.com' },
{ id: 6, name: '渡辺由美', department: '経理部', position: '係長', age: 34, salary: 5500000, joinDate: '2017-02-14', email: 'watanabe@company.com' },
{ id: 7, name: '中村雄介', department: '開発部', position: '部長', age: 42, salary: 7500000, joinDate: '2012-09-03', email: 'nakamura@company.com' },
{ id: 8, name: '小林智子', department: '人事部', position: '主任', age: 30, salary: 4600000, joinDate: '2019-11-12', email: 'kobayashi@company.com' }
];
// 列定義
const columnDefs = [
{
headerName: '選択',
checkboxSelection: true,
headerCheckboxSelection: true,
width: 50,
pinned: 'left'
},
{
headerName: 'ID',
field: 'id',
width: 80,
sort: 'asc'
},
{
headerName: '氏名',
field: 'name',
editable: true,
width: 120
},
{
headerName: '部署',
field: 'department',
editable: true,
filter: true,
cellEditor: 'agSelectCellEditor',
cellEditorParams: {
values: ['営業部', '開発部', '人事部', '経理部', '総務部']
}
},
{
headerName: '役職',
field: 'position',
editable: true,
filter: true,
cellEditor: 'agSelectCellEditor',
cellEditorParams: {
values: ['主任', '係長', '課長', '部長', '取締役']
}
},
{
headerName: '年齢',
field: 'age',
editable: true,
filter: 'agNumberColumnFilter',
width: 80
},
{
headerName: '給与',
field: 'salary',
editable: true,
filter: 'agNumberColumnFilter',
valueFormatter: params => {
return '¥' + params.value.toLocaleString();
},
cellStyle: { textAlign: 'right' }
},
{
headerName: '入社日',
field: 'joinDate',
editable: true,
filter: 'agDateColumnFilter',
cellEditor: 'agDateCellEditor'
},
{
headerName: 'メール',
field: 'email',
editable: true,
width: 200
}
];
// グリッドオプション
const gridOptions = {
columnDefs: columnDefs,
rowData: sampleData,
defaultColDef: {
sortable: true,
filter: true,
resizable: true,
floatingFilter: true
},
rowSelection: 'multiple',
animateRows: true,
pagination: true,
paginationPageSize: 10,
localeText: {
// 日本語ローカライゼーション
page: 'ページ',
more: 'その他',
to: '〜',
of: '件中',
next: '次へ',
last: '最後',
first: '最初',
previous: '前へ',
loadingOoo: '読み込み中...',
selectAll: 'すべて選択',
searchOoo: '検索...',
blanks: '空白',
filterOoo: 'フィルタ...',
applyFilter: 'フィルタ適用',
clearFilter: 'フィルタクリア',
equals: '等しい',
notEqual: '等しくない',
lessThan: '未満',
greaterThan: '超過',
inRange: '範囲内',
lessThanOrEqual: '以下',
greaterThanOrEqual: '以上',
contains: '含む',
notContains: '含まない',
startsWith: '開始',
endsWith: '終了'
},
onSelectionChanged: updateStats,
onRowDataChanged: updateStats,
onCellValueChanged: function(event) {
console.log('セルの値が変更されました:', event);
updateStats();
},
onCellClicked: function(event) {
// セル選択機能の実装
handleCellSelection(event);
},
onCellKeyDown: function(event) {
// キーボードナビゲーション
handleCellKeyNavigation(event);
}
};
// キーボードショートカットのイベントリスナー
document.addEventListener('keydown', function(event) {
// Ctrl+C または Cmd+C でコピー
if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
if (selectedCells.length > 0) {
copySelectedCellsToClipboard();
event.preventDefault();
}
}
// Escapeでセル選択クリア
if (event.key === 'Escape') {
clearCellSelection();
}
});
// セル選択の状態管理
let selectedCells = [];
let lastSelectedCell = null;
let isSelecting = false;
let selectionStart = null;
let selectionEnd = null;
let gridContainer = null;
// グリッドの初期化
let gridApi;
document.addEventListener('DOMContentLoaded', function() {
const gridDiv = document.querySelector('#myGrid');
gridApi = agGrid.createGrid(gridDiv, gridOptions);
gridContainer = gridDiv;
// 範囲選択のイベントリスナー追加
setupRangeSelectionEvents();
updateStats();
});
// 統計情報の更新
function updateStats() {
if (!gridApi) return;
const allData = [];
gridApi.forEachNode(node => allData.push(node.data));
const totalRows = allData.length;
const selectedRows = gridApi.getSelectedRows().length;
const avgAge = Math.round(allData.reduce((sum, row) => sum + row.age, 0) / totalRows);
const avgSalary = Math.round(allData.reduce((sum, row) => sum + row.salary, 0) / totalRows);
document.getElementById('statsInfo').innerHTML =
`総件数: ${totalRows}件 | 選択中: ${selectedRows}件 | 平均年齢: ${avgAge}歳 | 平均給与: ¥${avgSalary.toLocaleString()}`;
}
// 新規行追加
function addNewRow() {
const newId = Math.max(...sampleData.map(row => row.id)) + 1;
const newRow = {
id: newId,
name: '新規社員',
department: '営業部',
position: '主任',
age: 25,
salary: 3000000,
joinDate: new Date().toISOString().split('T')[0],
email: `newuser${newId}@company.com`
};
sampleData.push(newRow);
gridApi.setGridOption('rowData', sampleData);
updateStats();
}
// 選択行削除
function deleteSelected() {
const selectedRows = gridApi.getSelectedRows();
if (selectedRows.length === 0) {
alert('削除する行を選択してください。');
return;
}
if (confirm(`${selectedRows.length}件のデータを削除しますか?`)) {
selectedRows.forEach(row => {
const index = sampleData.findIndex(data => data.id === row.id);
if (index > -1) {
sampleData.splice(index, 1);
}
});
gridApi.setGridOption('rowData', sampleData);
updateStats();
}
}
// CSV出力
function exportToCsv() {
gridApi.exportDataAsCsv({
fileName: '社員データ.csv',
columnSeparator: ',',
suppressQuotes: false
});
}
// フィルタクリア
function clearFilters() {
gridApi.setFilterModel(null);
gridApi.setGlobalFilter('');
document.getElementById('quickSearch').value = '';
}
// クイック検索
function onQuickSearchChange() {
const searchText = document.getElementById('quickSearch').value;
gridApi.setGridOption('quickFilterText', searchText);
}
// セル選択処理
function handleCellSelection(event) {
// 範囲選択中の場合は処理しない
if (isSelecting) return;
// チェックボックス列は除外
if (event.column.getColId() === 'ag-Grid-AutoColumn') {
return;
}
const cellId = `${event.rowIndex}-${event.column.getColId()}`;
const cellElement = event.event.target.closest('.ag-cell');
if (!cellElement) return;
// Ctrlキーが押されている場合は複数選択
if (event.event.ctrlKey || event.event.metaKey) {
if (selectedCells.includes(cellId)) {
// 既に選択されている場合は解除
const index = selectedCells.indexOf(cellId);
selectedCells.splice(index, 1);
cellElement.classList.remove('selected-cell');
} else {
// 新しく選択
selectedCells.push(cellId);
cellElement.classList.add('selected-cell');
}
} else {
// 単一選択(既存の選択をクリア)
clearAllCellSelections();
selectedCells = [cellId];
cellElement.classList.add('selected-cell');
}
lastSelectedCell = {
rowIndex: event.rowIndex,
colId: event.column.getColId(),
value: event.value,
data: event.data
};
updateCellSelectionInfo();
}
// キーボードナビゲーション
function handleCellKeyNavigation(event) {
if (event.event.key === 'Escape') {
clearCellSelection();
}
}
// すべてのセル選択をクリア
function clearAllCellSelections() {
const selectedElements = document.querySelectorAll('.selected-cell, .range-selected, .range-selecting, .selection-start, .selection-end');
selectedElements.forEach(element => {
element.classList.remove('selected-cell', 'range-selected', 'range-selecting', 'selection-start', 'selection-end');
});
}
// セル選択クリア
function clearCellSelection() {
clearAllCellSelections();
selectedCells = [];
lastSelectedCell = null;
selectionStart = null;
selectionEnd = null;
isSelecting = false;
if (gridContainer) {
gridContainer.classList.remove('selecting');
}
updateCellSelectionInfo();
}
// セル選択情報の更新
function updateCellSelectionInfo() {
const infoDiv = document.getElementById('selectedCellInfo');
const detailsSpan = document.getElementById('cellDetails');
if (selectedCells.length === 0) {
infoDiv.style.display = 'none';
return;
}
infoDiv.style.display = 'block';
if (selectedCells.length === 1 && lastSelectedCell) {
const columnName = gridOptions.columnDefs.find(col => col.field === lastSelectedCell.colId)?.headerName || lastSelectedCell.colId;
detailsSpan.innerHTML = `
行: ${lastSelectedCell.rowIndex + 1},
列: ${columnName},
値: ${lastSelectedCell.value || '(空)'},
社員: ${lastSelectedCell.data.name}
`;
} else {
detailsSpan.innerHTML = `${selectedCells.length} 個のセルが選択されています`;
}
}
// 範囲選択イベントの設定
function setupRangeSelectionEvents() {
let mouseDownCell = null;
// マウスダウンイベント
gridContainer.addEventListener('mousedown', function(event) {
const cellElement = event.target.closest('.ag-cell');
if (!cellElement) return;
const rowElement = cellElement.closest('.ag-row');
if (!rowElement) return;
const rowIndex = parseInt(rowElement.getAttribute('row-index'));
const colId = cellElement.getAttribute('col-id');
console.log('Mouse down:', { rowIndex, colId, element: cellElement });
// チェックボックス列は除外
if (colId === 'ag-Grid-AutoColumn' || !colId) return;
// 左クリックのみ処理
if (event.button !== 0) return;
mouseDownCell = { rowIndex, colId, element: cellElement };
isSelecting = true;
selectionStart = { rowIndex, colId };
selectionEnd = { rowIndex, colId }; // 初期値として開始位置を設定
// 既存の選択をクリア(Ctrlキーが押されていない場合)
if (!event.ctrlKey && !event.metaKey) {
clearAllCellSelections();
selectedCells = [];
}
// 開始セルをマーク
cellElement.classList.add('selection-start');
gridContainer.classList.add('selecting');
console.log('Selection started:', selectionStart);
// ドラッグ時のデフォルト動作を防止
event.preventDefault();
});
// マウスムーブイベント
document.addEventListener('mousemove', function(event) {
if (!isSelecting || !mouseDownCell) return;
// グリッド内の要素かチェック
const elementUnderMouse = document.elementFromPoint(event.clientX, event.clientY);
if (!elementUnderMouse) return;
const cellElement = elementUnderMouse.closest('.ag-cell');
if (!cellElement) return;
const rowElement = cellElement.closest('.ag-row');
if (!rowElement) return;
const rowIndex = parseInt(rowElement.getAttribute('row-index'));
const colId = cellElement.getAttribute('col-id');
// チェックボックス列は除外
if (colId === 'ag-Grid-AutoColumn' || !colId) return;
// 新しい終了位置
const newSelectionEnd = { rowIndex, colId };
// 位置が変わった場合のみ更新
if (!selectionEnd ||
selectionEnd.rowIndex !== newSelectionEnd.rowIndex ||
selectionEnd.colId !== newSelectionEnd.colId) {
selectionEnd = newSelectionEnd;
console.log('Selection moved to:', selectionEnd);
// 範囲選択のプレビューを更新
updateRangeSelection();
}
});
// マウスアップイベント
document.addEventListener('mouseup', function(event) {
if (!isSelecting || !mouseDownCell) return;
console.log('Mouse up - finalizing selection');
// 範囲選択を確定
finalizeRangeSelection();
// 選択状態をリセット
isSelecting = false;
mouseDownCell = null;
gridContainer.classList.remove('selecting');
console.log('Selection finalized, total cells:', selectedCells.length);
});
}
// 範囲選択の更新
function updateRangeSelection() {
if (!selectionStart || !selectionEnd) {
console.log('Missing selection start or end');
return;
}
console.log('Updating range selection:', { start: selectionStart, end: selectionEnd });
// 既存の範囲選択スタイルをクリア
const existingRangeElements = document.querySelectorAll('.range-selecting');
existingRangeElements.forEach(element => {
element.classList.remove('range-selecting');
});
// 範囲を計算
const startRow = Math.min(selectionStart.rowIndex, selectionEnd.rowIndex);
const endRow = Math.max(selectionStart.rowIndex, selectionEnd.rowIndex);
// 列の順序を取得
const columnOrder = [];
gridApi.getColumnState().forEach(col => {
if (!col.hide && col.colId !== 'ag-Grid-AutoColumn') {
columnOrder.push(col.colId);
}
});
const startColIndex = columnOrder.indexOf(selectionStart.colId);
const endColIndex = columnOrder.indexOf(selectionEnd.colId);
if (startColIndex === -1 || endColIndex === -1) {
console.log('Column not found in order');
return;
}
const minColIndex = Math.min(startColIndex, endColIndex);
const maxColIndex = Math.max(startColIndex, endColIndex);
console.log('Range:', { startRow, endRow, minColIndex, maxColIndex });
// 範囲内のセルにスタイルを適用
let cellsApplied = 0;
for (let row = startRow; row <= endRow; row++) {
for (let colIndex = minColIndex; colIndex <= maxColIndex; colIndex++) {
const colId = columnOrder[colIndex];
if (colId) {
const cellElement = getCellElement(row, colId);
if (cellElement) {
cellElement.classList.add('range-selecting');
cellsApplied++;
}
}
}
}
console.log('Applied range-selecting to', cellsApplied, 'cells');
}
// セル要素を確実に取得する関数
function getCellElement(rowIndex, colId) {
// 複数の方法でセル要素を探す
let cellElement = document.querySelector(`[row-index="${rowIndex}"][col-id="${colId}"]`);
if (!cellElement) {
// 代替方法:行要素を取得してから列を探す
const rowElement = document.querySelector(`[row-index="${rowIndex}"]`);
if (rowElement) {
cellElement = rowElement.querySelector(`[col-id="${colId}"]`);
}
}
if (!cellElement) {
// さらなる代替方法:すべてのセルを検索
const allCells = document.querySelectorAll('.ag-cell');
for (let cell of allCells) {
const row = cell.closest('.ag-row');
if (row &&
parseInt(row.getAttribute('row-index')) === rowIndex &&
cell.getAttribute('col-id') === colId) {
cellElement = cell;
break;
}
}
}
return cellElement;
}
// 範囲選択の確定
function finalizeRangeSelection() {
if (!selectionStart || !selectionEnd) {
console.log('Cannot finalize - missing start or end');
return;
}
console.log('Finalizing range selection:', { start: selectionStart, end: selectionEnd });
// 範囲を計算
const startRow = Math.min(selectionStart.rowIndex, selectionEnd.rowIndex);
const endRow = Math.max(selectionStart.rowIndex, selectionEnd.rowIndex);
// 列の順序を取得
const columnOrder = [];
gridApi.getColumnState().forEach(col => {
if (!col.hide && col.colId !== 'ag-Grid-AutoColumn') {
columnOrder.push(col.colId);
}
});
const startColIndex = columnOrder.indexOf(selectionStart.colId);
const endColIndex = columnOrder.indexOf(selectionEnd.colId);
if (startColIndex === -1 || endColIndex === -1) {
console.log('Column not found during finalization');
return;
}
const minColIndex = Math.min(startColIndex, endColIndex);
const maxColIndex = Math.max(startColIndex, endColIndex);
// 選択されたセルのリストを作成
const newSelectedCells = [];
for (let row = startRow; row <= endRow; row++) {
for (let colIndex = minColIndex; colIndex <= maxColIndex; colIndex++) {
const colId = columnOrder[colIndex];
if (colId) {
const cellId = `${row}-${colId}`;
newSelectedCells.push(cellId);
const cellElement = getCellElement(row, colId);
if (cellElement) {
cellElement.classList.remove('range-selecting', 'selection-start', 'selection-end');
cellElement.classList.add('range-selected');
}
}
}
}
console.log('New selected cells:', newSelectedCells);
// 選択状態を更新
selectedCells = [...new Set([...selectedCells, ...newSelectedCells])];
// 最後の選択情報を更新
const endRowData = getRowData(selectionEnd.rowIndex);
if (endRowData) {
lastSelectedCell = {
rowIndex: selectionEnd.rowIndex,
colId: selectionEnd.colId,
value: endRowData[selectionEnd.colId],
data: endRowData
};
}
updateCellSelectionInfo();
console.log('Final selected cells count:', selectedCells.length);
}
// 列インデックスを取得
function getColumnIndex(colId) {
const visibleColumns = gridApi.getColumnState().filter(col => col.hide !== true);
return visibleColumns.findIndex(col => col.colId === colId);
}
// インデックスから列IDを取得
function getColumnIdByIndex(index) {
const visibleColumns = gridApi.getColumnState().filter(col => col.hide !== true);
return visibleColumns[index]?.colId;
}
// 行データを取得
function getRowData(rowIndex) {
let rowData = null;
gridApi.forEachNode((node, index) => {
if (index === rowIndex) {
rowData = node.data;
}
});
return rowData;
}
// 選択したセルをクリップボードにコピー
async function copySelectedCellsToClipboard() {
if (selectedCells.length === 0) {
alert('コピーするセルを選択してください。');
return;
}
try {
// 選択されたセルを行・列でソート
const cellData = [];
// セル情報を収集
selectedCells.forEach(cellId => {
const [rowIndex, colId] = cellId.split('-');
const rowData = getRowData(parseInt(rowIndex));
if (rowData) {
cellData.push({
row: parseInt(rowIndex),
col: colId,
value: rowData[colId] || ''
});
}
});
// 列の順序を取得
const columnOrder = [];
gridApi.getColumnState().forEach(col => {
if (!col.hide && col.colId !== 'ag-Grid-AutoColumn') {
columnOrder.push(col.colId);
}
});
// セルデータを行・列順でソート
cellData.sort((a, b) => {
if (a.row !== b.row) {
return a.row - b.row;
}
return columnOrder.indexOf(a.col) - columnOrder.indexOf(b.col);
});
// 2次元配列として整理
const grid = {};
cellData.forEach(cell => {
if (!grid[cell.row]) {
grid[cell.row] = {};
}
grid[cell.row][cell.col] = cell.value;
});
// タブ区切りテキストを作成
let clipboardText = '';
const rows = Object.keys(grid).map(Number).sort((a, b) => a - b);
rows.forEach((rowIndex, i) => {
const rowData = grid[rowIndex];
const rowCells = [];
// この行に含まれる列を取得
const rowColumns = Object.keys(rowData);
rowColumns.sort((a, b) => columnOrder.indexOf(a) - columnOrder.indexOf(b));
// 連続する列範囲を作成
if (rowColumns.length > 0) {
const firstColIndex = columnOrder.indexOf(rowColumns[0]);
const lastColIndex = columnOrder.indexOf(rowColumns[rowColumns.length - 1]);
for (let colIndex = firstColIndex; colIndex <= lastColIndex; colIndex++) {
const colId = columnOrder[colIndex];
const value = rowData[colId];
if (value !== undefined) {
// 値の型に応じてフォーマット
let formattedValue = value;
if (typeof value === 'number') {
// 数値の場合はそのまま
formattedValue = value.toString();
} else if (value === null || value === undefined) {
formattedValue = '';
} else {
// 文字列の場合、タブや改行をエスケープ
formattedValue = value.toString().replace(/\t/g, ' ').replace(/\n/g, ' ');
}
rowCells.push(formattedValue);
} else {
rowCells.push('');
}
}
}
clipboardText += rowCells.join('\t');
if (i < rows.length - 1) {
clipboardText += '\n';
}
});
console.log('Copying to clipboard:', clipboardText);
// クリップボードにコピー
await navigator.clipboard.writeText(clipboardText);
// 成功メッセージを表示
showCopyNotification(selectedCells.length);
} catch (error) {
console.error('コピーに失敗しました:', error);
// フォールバック: テキストエリアを使用
fallbackCopyToClipboard();
}
}
// コピー成功の通知を表示
function showCopyNotification(cellCount) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4caf50;
color: white;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
font-family: inherit;
animation: slideIn 0.3s ease-out;
`;
notification.innerHTML = `📋 ${cellCount}個のセルをコピーしました`;
// アニメーション用CSS
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// 3秒後に削除
setTimeout(() => {
notification.style.animation = 'slideIn 0.3s ease-out reverse';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
if (style.parentNode) {
style.parentNode.removeChild(style);
}
}, 300);
}, 3000);
}
// フォールバック: テキストエリアを使用したコピー
function fallbackCopyToClipboard() {
// 簡単な形式でコピー
const values = selectedCells.map(cellId => {
const [rowIndex, colId] = cellId.split('-');
const rowData = getRowData(parseInt(rowIndex));
return rowData ? (rowData[colId] || '') : '';
});
const textArea = document.createElement('textarea');
textArea.value = values.join('\t');
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopyNotification(selectedCells.length);
} catch (err) {
alert('コピーに失敗しました。ブラウザがクリップボードアクセスをサポートしていない可能性があります。');
}
document.body.removeChild(textArea);
}
// グリッドの再描画時にセル選択を復元
function restoreCellSelections() {
setTimeout(() => {
selectedCells.forEach(cellId => {
const [rowIndex, colId] = cellId.split('-');
const cellElement = document.querySelector(
`[row-index="${rowIndex}"][col-id="${colId}"]`
);
if (cellElement) {
cellElement.classList.add('range-selected');
}
});
}, 100);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment