Last active
July 15, 2025 03:23
-
-
Save nnao45/f74613dc2971029e31225dcd2bf5a511 to your computer and use it in GitHub Desktop.
ag-grid community cell selection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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