IgbGrid には行や列を選択してコピーする機能は ClipboardOptions により標準で用意されていますが、Excel などからコピーしたデータをグリッドへ貼り付ける機能は標準では実装されていません。本記事では、ブラウザ標準の paste イベントを利用してクリップボードのデータを取得し、updateRow() メソッドで既存の行に反映するカスタム JavaScript を実装することで、Excelからのデータ貼り付けに対応する方法を紹介します。

IgbGrid の設定

まず、貼り付け先となる IgbGrid に IdPrimaryKey を設定します。これらは後述のカスタム JavaScript から、対象のグリッド要素を特定し、行を一意に識別するために使用します。

<IgbGrid AutoGenerate="false"
         Id="@GridId"
         Data="GridData"
         PrimaryKey="ID"
         ClipboardOptions="clipboardOptions">
    <IgbColumn Field="LastName" Header="姓" ...></IgbColumn>
    <IgbColumn Field="FirstName" Header="名" ...></IgbColumn>
    <IgbColumn Field="Country" Header="国" ...></IgbColumn>
</IgbGrid>

次に、コンポーネントの初回レンダー後に貼り付け処理を実装した JavaScript モジュールを読み込み、.NET 側の参照を渡してグリッドの初期化を行います。

private const string GridId = "myGrid1";
private DotNetObjectReference<Component>? dotNetHelper;
private IJSObjectReference? _jsModule;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "import", "./Components/Component.razor.js");

        dotNetHelper = DotNetObjectReference.Create(this);
        await _jsModule.InvokeVoidAsync("setDotNetHelper", GridId, dotNetHelper);
        await _jsModule.InvokeVoidAsync("initGrid", GridId);
    }
}

貼り付け処理の実装

IgbGrid はセルにフォーカスがある状態での Ctrl+V を自動で解釈してくれないため、Ctrl+V(または Shift+Insert)を検知したタイミングで、画面外に隠した textarea へフォーカスを移し、その要素で発生する標準の paste イベントを利用してクリップボードの生データを取得します。

const getTextArea = (gridId) => {
    const instance = getInstance(gridId);
    if (!instance.textArea) {
        const txtArea = document.createElement("textarea");
        ...
        txtArea.addEventListener("paste", (eventArgs) => { onPaste(eventArgs, gridId); });
        instance.textArea = txtArea;
    }
    return instance.textArea;
};

const onWebGridPasteFromExcelKeyDown = (eventArgs, gridId) => {
    const ctrl = eventArgs.ctrlKey;
    const key = eventArgs.keyCode;
    if ((ctrl || eventArgs.metaKey) && key === 86 || eventArgs.shiftKey && key === 45) {
        getTextArea(gridId).focus();
    }
};

隠し textareapaste イベントが発生したら、clipboardData.getData() でテキストを取得します。Excel からコピーしたデータは行が改行(\n)、列がタブ(\t)で区切られたテキストになるため、この形式に合わせて2次元配列へ分解します。

const onPaste = (eventArgs, gridId) => {
    eventArgs.preventDefault();
    const data = eventArgs.clipboardData.getData("text/plain");
    const processedData = processData(data);
    updateRecords(processedData, gridId);
};

const processData = (data) => {
    const pasteData = data.split("\n").map(row => row.split("\t"));

    const lastRow = pasteData[pasteData.length - 1];
    if (lastRow.length === 1 && lastRow[0] === "") {
        pasteData.pop();
    }

    pasteData.forEach(row => {
        const lastIndex = row.length - 1;
        if (typeof row[lastIndex] === 'string') {
            row[lastIndex] = row[lastIndex].replace(/\r$/, '');
        }
    });
    return pasteData;
};

整形した2次元配列は、貼り付け実行時に選択されていたセル(grid.selectedCells[0])を起点として、既存の行・列にのみ反映します。選択セルより左側の列は元の値を保持し、右側の列だけを貼り付けたデータで置き換え、updateRow() メソッドでグリッドを更新したうえで、更新結果を invokeMethodAsync() により .NET 側へ渡します。

const updateRecords = (processedData, gridId) => {
    const grid = document.querySelector("#" + gridId);
    const cell = grid.selectedCells[0];
    const pk = grid.primaryKey;
    if (!cell) { return; }
    const rowIndex = cell.row.index;
    const columns = grid.visibleColumns;
    const cellIndex = grid.visibleColumns.indexOf(cell.column);
    let index = 0;
    const updatedRecs = [];
    for (const curentDataRow of processedData) {
        const dataRec = grid.data[rowIndex + index];
        if (!dataRec) { continue; }

        const rowData = {};
        const rowPkValue = dataRec[pk];
        rowData[pk] = rowPkValue;
        for (let j = 0; j < columns.length; j++) {
            let currentCell;
            if (j >= cellIndex) {
                currentCell = curentDataRow.shift();
            }
            const colKey = columns[j].field;
            rowData[colKey] = currentCell ?? dataRec[colKey];
        }

        grid.updateRow(rowData, rowPkValue);
        updatedRecs.push(rowData);
        index++;
    }
    getInstance(gridId).dotNetHelper?.invokeMethodAsync('SyncData', updatedRecs);
};

JavaScript 側から渡された更新内容は、[JSInvokable] 属性を付けた SyncData() メソッドで受け取り、Blazor 側で保持しているグリッドのデータモデルに反映します。

[JSInvokable]
public Task SyncData(List<RowData> updatedRows)
{
    if (GridData == null)
    {
        return Task.CompletedTask;
    }

    Dictionary<string, RowData> lookup = GridData
        .Where(r => r.ID != null)
        .ToDictionary(r => r.ID!);

    foreach (RowData updated in updatedRows)
    {
        if (updated.ID != null && lookup.TryGetValue(updated.ID, out RowData? record))
        {
            record.FirstName = updated.FirstName?.Replace(oldValue: "\r", newValue: "");
            record.LastName = updated.LastName?.Replace(oldValue: "\r", newValue: "");
            record.Country = updated.Country?.Replace(oldValue: "\r", newValue: "");
        }
    }

    return Task.CompletedTask;
}

実行結果

グリッド上でセルを選択した状態で Excel のセル範囲をコピーし、Ctrl+V(または Shift+Insert)で貼り付けると、選択したセルを起点にコピーしたデータがグリッドへ反映されます。

Excel のデータをコピーして、

グリッドにペーストすることができます。

Tagged:

製品について

Ignite UI for Blazor